Home

Awesome

<p align=center><img width=700 src=https://user-images.githubusercontent.com/2152766/72180447-1c13ba80-33df-11ea-99f0-1dc1dae2f60c.png></p> <p align=center>A single-file, immediate-mode sequencer widget for C++17, Dear ImGui and EnTT</p> <p align=center><img width=800 src=https://user-images.githubusercontent.com/47274066/72092588-8d356e00-330a-11ea-8e83-007bb59aaf45.png></p> <br>

Table of Contents

<br> <br>

Try it

You could build it, or you can download a pre-built version and fool around.

Quickstart

KeyDescription
QWERSwitch between tools
Click + DragManipulate the coloured squares
Click + DragMove events around in time
Alt + Click + DragPan in the Event Editor
DeleteDelete all events
SpacePlay and pause
F1Toggle the ImGui performance window
F2Toggle the Theme Editor
Backspace(debug) Pause the rendering loop
Enter(debug) Redraw one frame
<br> <br>

Overview

It's an sequence editor, in the spirit of MIDI authoring software like Ableton Live, Bitwig and FL Studio, where each event carry a start time, a duration and handle to your custom application data.

Heads up!

This is a work in progress, alpha at best, and is going through changes (that you are welcome to participate in!)

What makes Sequentity different, and inspired its name, is that it is built as an Entity-Component-System (ECS), and events are a combination of start time, length and custom application data; as opposed to individual events for start and end; suitable for e.g. (1) create a dynamic rigid body, (2) edit said body, whilst maintaining reference to what got created and (3) delete the body at the end of the event.

entt::registry& registry;

auto entity = registry.create();
auto& track = registry.assign<Sequentity::Track>(entity, "My first track");
auto& channel = Sequentity::PushChannel(track, MyEventType, "My first channel");
auto& event = Sequentity::PushEvent(channel, 10, 5); // time, length

while (true) {
    ImGui::Begin("Event Editor");
    Sequentity::EventEditor(registry);
    ImGui::End();
}

What can I use it for?

If you need to record anything in your application, odds are you need to play something back. If so, you may also need to edit what got recorded, in which case you can use something like Sequentity.

I made this for recording user input in order to recreate application state exactly such that I could record once more, on-top of the previous recording; much like how musicians record over themselves with various instruments to produce a complete song. You could theoretically decouple the clock-time aspect and use this as playback mechanism for undo/redo, similar to what ZBrush does, and save that with your scene/file. Something I intend on experimenting with!

Goals

Is there anything similar?

I'm sure there are, however I was only able to find one standalone example, and only a few others embedded in open source applications.

If you know any more, please let me know by filing an issue!

Finally, there are others with a similar interface but different implementation and goal.

<br> <br>

Features

sequentitydemo1 sequentitydemo3 sequentitydemo2 sequentity_zooming sequentitydemo4 sequentitydemo6

<br> <br>

Design Decisions

* The difference being that a sample is a complete snapshot of your application/game state, whereas a frame is a (potentially fractal) point in time, e.g. 1.351f

<br> <br>

Todo

These are going into GitHub issues shortly.

<br> <br>

Open Questions

I made Sequentity for another (commercial) project, but made it open source in order to seek help from the open source community. This is my first sequencer-like project and in fact my first C++ project (with <4 months of experience using the language), so I expect lots of things to be ripe for improvement.

Here are some of the things I'm actively looking for answers to and that you are welcome to strike up a dialog about in a new issue. (Thank you!)

On top of these, there are some equivalent Application Open Questions for the Tools and Input handling which I would very much like your feedback on.

<br> <br>

Install

Sequentity is distributed as a single-file library, with the .h and .cpp files combined.

  1. Copy Sequentity.h into your project
  2. #define SEQUENTITY_IMPLEMENTATION in one of your .cpp files
  3. #include <Sequentity.h>
  4. See below

Dependencies

<br> <br>

Usage

Sequentity can draw events in time, and facilitate edits to be made to those events interactively by the user. It doesn't know nor care about playback, that part is up to you.

<details><summary>New to <b><a href="https://github.com/skypjack/entt">EnTT</a></b>?</summary>

An EnTT Primer

Here's what you need to know about EnTT in order to use Sequentity.

  1. EnTT (pronounced "entity") is an ECS framework
  2. ECS stands for Entity-Component-System
  3. Entities are identifiers for "things" in your application, like a character, a sound or UI element
  4. Components carry the data for those things, like the Color, Position or Mesh
  5. Systems operate on that data in some way, such as adding +1 to Position.x each frame

It works like this.

// You create a "registry"
auto registry = entt::registry;

// Along with an entity
auto entity = registry.create();

// Add some data..
struct Position {
    float x { 0.0f };
    float y { 0.0f };
};
registry.assign<Position>(entity, 5.0f, 1.0f);  // 2nd argument onwards passed to constructor

// ..and then iterate over that data
registry.view<Position>().each([](auto& position) {
    position.x += 1.0f;
});

A "registry" is what keeps track of what entities have which components assigned, and "systems" can be as simple as a free function. I like to think of each loop as its own system, like that one up there iterating over positions. Single reponsibility, and able to perform complex operations that involve multiple components.

Speaking of which, here's how you combine components.

registry.view<Position, Color>().each([](auto& position, const auto& color) {
    position.x += color.r;
});

This function is called on every entity with both a position and color, and combines the two.

Sequentity then is just another component.

registry.assign<Sequentity::Track>(entity);

This component then stores all of the events related to this entity. When the entity is deleted, the Track is deleted alongside it, taking all of the events of this entity with it.

registry.destroy(entity);

You could also keep the entity, but erase the track.

registry.remove<Sequentity::Track>(entity);

And when you're fed up with entities and want to go home, then just:

registry.clear();

And that's about it as far as Sequentity goes, have a look at the EnTT Wiki along with my notes for more about EnTT. Have fun!

</details>

Here's how you draw.

// Author some data
entt::registry& registry;
entity = registry.create();

// Events may carry application data and a type for you to identify it with
struct MyEventData {
    float value { 0.0f };
};

enum {
    MyEventType = 0
};

auto& track = registry.assign<Sequentity::Track>(entity); {
    track.label = "My first track";
    track.color = ImColor::HSV(0.0f, 0.5f, 0.75f);
}

auto& channel = Sequentity::PushChannel(track, MyEventType); {
    channel.label = "My first channel";
    channel.color = ImColor::HSV(0.33f, 0.5f, 0.75f);
}

auto& event = Sequentity::PushEvent(channel); {
    event.time = 1;
    event.length = 50;
    event.color ImColor::HSV(0.66f, 0.5f, 0.75f);
}

// Draw it!
Sequentity::EventEditor(registry);

And here's how you query.

const int time { 13 };
Sequentity::Intersect(track, time, [](const auto& event) {
    if (event.type == MyEventType) {

        // Do something interesting
        event.time;
        event.length;
    }
});

The example application uses events for e.g. translations, storing a vector of integer pairs representing position. For each frame, data per entity is retrieved from the current event and correlated to a position by computing the time relative the start of an event.

<br>

Event Handlers

What you do with events is up to you, but I would recommend you establish so-called "event handlers" for the various types you define.

For example, if you define Translate, Rotate and Scale event types, then you would need:

  1. Something to produce these
  2. Something to consume these

Producers in the example applications are so-called "Tools" and operate based on user input like the current mouse position. The kind of tool isn't necessarily bound or even related to the type of event it produces. For example, a TranslateTool would likely generate events of type TranslateEvent with TranslateEventData, whereby you may establish an equivalent TranslateEventHandler to interpret this data.

enum EventTypes_ : Sequentity::EventType {
    TranslateEvent = 0;
};

struct TranslateEventData {
    int x;
    int y;
};

void TranslateEventHandler(entt::entity entity, const Sequentity::Event& event, int time) {
    auto& position = Registry.get<Position>(entity);
    auto& data = static_cast<TranslateEventData*>(event.data);
    // ...
}
<br>

Sorting

Tracks are sorted in the order of their EnTT pool.

Registry.sort<Sequentity::Track>([this](const entt::entity lhs, const entt::entity rhs) {
    return Registry.get<Index>(lhs) < Registry.get<Index>(rhs);
});
<br>

State

State - such as the zoom level, scroll position, current time and min/max range - is stored in your EnTT registry which is (optionally) accessible from anywhere. In the example application, it is used to draw the Transport panel with play, stop and visualisation of current time.

auto& state = registry.ctx<Sequentity::State>();

When state is automatically created by Sequentity if you haven't already done so. You may want to manually create state for whatever reason, which you can do like this.

auto& state = registry.set<Sequentity::State>();
state.current_time = 10;

// E.g.
Sequentity::EventEditor(registry);

To draw the event editor with the current time set to 10.

<br>

Components

Sequentity provides 1 ECS component, and 2 additional inner data structures.

/**
 * @brief A Sequentity Event
 *
 */
struct Event {
    TimeType time { 0 };
    TimeType length { 0 };

    ImVec4 color { ImColor::HSV(0.0f, 0.0f, 1.0f) };

    // Map your custom data here, along with an optional type
    EventType type { EventType_Move };
    void* data { nullptr };

    /**
     * @brief Ignore start and end of event
     *
     * E.g. crop = { 2, 4 };
     *       ______________________________________
     *      |//|                              |////|
     *      |//|______________________________|////|
     *      |  |                              |    |
     *      |--|                              |----|
     *  2 cropped from start             4 cropped from end
     *
     */
    TimeType crop[2] { 0, 0 };

    /* Whether or not to consider this event */
    bool enabled { true };

    /* Events are never really deleted, just hidden from view and iterators */
    bool removed { false };

    /* Extend or reduce the length of an event */
    float scale { 1.0f };

    // Visuals, animation
    float height { 0.0f };
    float thickness { 0.0f };

};

/**
 * @brief A collection of events
 *
 */
struct Channel {
    const char* label { "Untitled channel" };

    ImVec4 color { ImColor::HSV(0.33f, 0.5f, 1.0f) };

    std::vector<Event> events;
};


/**
 * @brief A collection of channels
 *
 */
struct Track {
    const char* label { "Untitled track" };

    ImVec4 color { ImColor::HSV(0.66f, 0.5f, 1.0f) };

    bool solo { false };
    bool mute { false };

    std::unordered_map<EventType, Channel> channels;
    
    // Internal
    bool _notsoloed { false };
};
<br>

Serialisation

All data comes in the form of components with plain-old-data, including state like panning and zooming.

TODO

<br>

Roadmap

See Todo for now.

<img width=600 src=https://user-images.githubusercontent.com/2152766/72179629-39e02000-33dd-11ea-929f-1196c2eed2f5.png>