Damped Values: General Function / Procedure

I am following a generic function / procedure that will calculate decay times and values ​​for me based on the data provided, something like this:

I have byte values ​​stored in a byte array: these are the initial values. Then I have some memorized values ​​in another array: these are the new values. I then have a grant time, which takes a start time to get to the new value.

I need to get updated values ​​every time they change (0.1 second precision). I know that if A's value changes to 10 and B's value changes to 100 at the same time, say 1 second, I will get A's value updated 10 times and B's value will be updated 100 times. Until now, I was planning to use a let say interval timer of 50ms, which will constantly calculate the difference based on the change value and the time required, e.g change step := (Difference between start and new value / {divided by} (fade time / timer interval) )

. : .

But given the fact that the changes to the values ​​are different also slows down the time and that I can execute a different value fading out before the first decay is over, makes the whole thing confusing and difficult for me.

So, I need an option, let's say that the values ​​in indices 1, 2, 3, 4, 5, 6 and 7 are changed to new values ​​in 30 seconds, then at some point somewhere between me it was possible to order values ​​in indices 11, 13 and 17 to change their new values ​​in 9 seconds, etc.

Also, in case the A value will have a fade in relation to the B value, and the other will fade from A to C, I would like it to be added to the queue list to be executed right after the first Fade out is over. And at that time, the B value from the first team will become the A value in the second team. It has to do with these facts: A in the above example should always be read at the very moment the decay starts. So this is the starting value no matter what was done before the fade or between the fade command and the fade. So I could set Fade1 to Current → B @ 10s and queue Fade2 to Current → C @ 10s, whereas the current in the second case is actually the value, otherwise stored as B, and let's assume that Current in Fade1 is the same. how the value is stored as C. Sothe value will be in a loop that changes every 10 seconds. So basically the command to add fade should only have something like SetNewFade: Dest: = B; Time:. = 10;

So I could add -> B @ 10s, -> C @ 10s, -> B @ 10s, -> C @ 10s, and it will just go from B to C and back until the queue list is empty. I hope I was able to make this clear enough for you to understand. I really cannot better describe what I need to achieve.

Also, since all fades will be rendered in the Listbox, I would like to be able to remove the fades in the queue at will. But if the currently removed fade is removed, the value should transition to the new value, as if the fade had already completed and typically then triggered a new fade in the queue list, if any.

What is the easiest way to create this? Is using a fixed interval timer a good idea? Could this cause any delays if many values ​​are expected to fade? Uses dynamic arrays for values ​​and times (and fills them on the StartFade event and releases them after the fade completes) a shot in the dark or a good guess?

Here's an example that I hope makes it clearer:

A: array [0..20] of Byte;
B: array [0..20] of Byte;
C: array [0..20] of Byte;
Current: array [0..20] of Byte;

      

Button1 applies A values ​​to the current values, Button2 applies B values, Button3 applies C values, and so on ...

So, I set the time in the "Edit" field to say 5 seconds and click the "Button1" button. With this, I have added fade from Current to the values ​​in array A with a time of 5 seconds. Since it is first in line, it starts executing immediately. Before the fade is complete, I set the time to 20 seconds and press button 2. So I just added another fade in the queue list: from Current to the values ​​in array B. Since I change the same values ​​(index 0..20) this starts to execute correctly after the first fade finishes. Note: The fade process continually updates the current array until it has the same values ​​as the array of Fade commands! Consequently, the second disappearance will again disappear from Current to B, with the current actually being the same as A.

Now that things get even more complicated, I actually only set the values ​​indexed 0, 1, 2, 3, and 4 from the arrays to be faded @ 5sec to A, and then I apply the values ​​indexed 5, 6, 7. 8 and 9 should be faded values ​​from 10 seconds to B: in this case, since the indices I'm fading are different from each other, both fade commands should be executed at once .

If one value is in both decays (for example, if I added a value indexed by 4 to the second decay), only that value would need to be added to the queue list. Thus, the other immediately disappears, and the one that already fades out at the first disappearance, waits for its completion, and then begins to disappear in accordance with the second command.

Additional Information:

  • The length of the arrays is not currently fixed, but can be fixed if it is important. This is for sure a multiplier of 512 with a maximum of 2047.

  • The number of arrays is unknown and should be changed at runtime as needed. Probably, they are stored as records (e.g. StoredVals: array of record;

    , Index: array of Integer

    (the index value, it means any value stored in this entry), and Value: array of Byte;

    (this is the actual values which have disappeared, for example, based on Current[StoredVals[0].Index[0]]

    . Current stores data all the values between the entry A , B, C, etc. only store the values ​​of those indexed within that record).

  • The lengths of the arrays based on the above description are not always equal as they do not always change the same number of values.

  • Arrays are populated from the database on initialization. Since they can be created at runtime, they are populated from the current values ​​and stored as a new array. But this is always also written to the database and then. These are kind of memorized values, so I can remember them with buttons. For that matter, I would like to be able to recall these values ​​immediately (as it is now) or via a fade option. Now, to avoid problems for the value in the queue, I thought about sending this immediate change also through the fade process, only with a time of 0 seconds. This way, values ​​that are not in the queue will be applied immediately, and if some value is currently fading, it will be applied after that fade ends. Nevertheless,this decay process will reside in the command stream.

If you need any other further clarification, please do not hesitate to ask!

I know this is really difficult and therefore I am looking for your help. Any partial help or suggestions would be appreciated.

+3


source to share


1 answer


I am following a general function / procedure ...

Actually, you seem to be after a complete program. You think about the solution in general, and that the clouding is, which is why you have so many questions. You need to learn how to break this task down into smaller parts and state the requirements more clearly. The question as it stands is close to being off-topic and is probably better suited for SE programmers . But since this is coming up my lane, I would like to let you pass.

Requirements

  • There is a set of values ​​X of length N.
  • One or more values ​​in this set can be assigned a new value.
  • The modification from the old value to the new value must be done in increments over a specified duration.
  • This results in intermediate values ​​during this transition.
  • This transition has a value / index, i.e. duration for X [0] may differ from time for X [ 1].
  • The transition must be fully completed before another new value can be assigned.
  • New values ​​can be requested for assignment during the transition.
  • This concludes that new requests should be kept in the queue, so that when the transition is complete, a new request for a value can be popped off the queue, resulting in a new transition.

I'm sure this is the correct summary of your question.

Transitions

Your suggestion to use a timer to perform a portion of the overall transition at each interval is audible. There are now two ways to calculate these chunks:

  • Divide the total transition by a fixed number of small transitions, set the timer interval by the total duration divided by that number, and process the sum of all processed smaller transitions at each interval. This is what you suggest when calculating the step of change. The disadvantage of this method is twofold:
    • The timer interval is approximate and will not be accurate due to various reasons, one of which depends on the Windows messaging model, for which "the time depends on many processes, including yours,
    • Possible rough or uneven progress because of this.
  • Recalculate the portion of the processed transition at each interval. This way, the progress will always be smooth, whether the next interval is twice as long or not.

The second solution is preferred and that means the following general procedure you are looking for. Let's start simple if we take one element:

function InBetweenValue(BeginValue, EndValue: Byte; Duration, 
  StartTick: Cardinal): Byte
var
  Done: Single;
begin
  Done := (GetTickCount - StartTick) / Duration;
  if Done >= 1.0 then
    Result := EndValue
  else
    Result := Round(BeginValue + (EndValue - BeginValue) * Done);
end;

      

Is using a fixed interval timer a good idea?

With this approach, the timer interval does not affect the calculation: at any time, the result InBetweenValue

will be correct. The only thing Timer needs is to advance progress. If you want a refresh rate of 67 Hz, set the time interval to 15 milliseconds. If you want a refresh rate of 20 Hz, set the interval to 50 milliseconds.

Performance

Could it be causing any delays if many values ​​are expected to fade?

No, not for a reason. The time required for all computation may depend on the size of the queue, but this is most likely not a significant factor. (If so, then you have problems with a much more disturbing caliber.) Potential "delays" will result in lower refresh rates due to missing or concatenated Windows Timer messages, depending on how busy the computer is with everything it does.

Data storage

Uses dynamic arrays for values ​​and times (and fills them in the "StartFade" event and releases them after the fade completes), shot in the dark, or a good guess?

First, analyze what data needs to be processed. There is one set of intermediate current values ​​of arbitrary length, and each value has its four attributes: start value, end value, transition duration, and transition start time. Thus, you have a choice between:

  • Storing 5 sets: one set of current values ​​and four sets of attributes, or
  • Store 1 set: one set of current values, where each value has four attribute elements.


The first solution requires synchronization problems for all five sets. The second requires a different dimension. I would prefer the latter.

Whether you are using arrays or anything else for you. Choose what you like best, what suits the purpose or what is the best for the input or required output. Whether you choose static dynamic arrays depends on the volatility of the input and does not result in a noticeable difference in performance. Dynamic arrays require runtime length control where there are no static arrays.

But since you still need a dynamic solution, I suggest thinking outside the box. For example, RTL doesn't offer any built-in default management tools for arrays, but it does have collection classes that do eg. TList

...

For the rest of this answer, I'll be making the decision to use an object for an item and a list to keep track of them.

Design

Now that the two most relevant points have been considered, the design can be developed.

There is a list with elements, and each element has its current value and four attributes: start, end, duration and start time. Each element should be able to receive new attribute values. There is a formula to calculate the current value based on attributes. And there is a Timer that should automate several of these calculations.

In addition, several transition commands must be saved for the element. Since we already have an element with members, add these commands as an element element too.

Is there something missing? Not. Let go.

Implementation

We need:

  • Type for a transition with two members: end value and duration,
  • A type for a multiple of these transitions, preferably with queue characteristics,
  • Element type with six members: start value, end value, duration, start time, current value and transitions,
  • A type for a list of such elements,
  • The procedure for calculating the current value of an element,
  • The procedure for the appearance of a new transition when the current value has reached the final value,
  • A subroutine to do this calculation and enter all the elements,
  • Timer to start this general procedure,
  • Procedure for updating Item attributes. Let's repeat. Do we need to set all the attributes? Is transition required for all parameters?
  • An object type that contains all of this together.

This will help you customize part of the interface. Delay and keep the urge to start coding the implementation.

So my attempt, arose as described above:

unit Modulation;

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, WinAPI.Windows,
  VCL.ExtCtrls;

type
  TTransition = record
    EndValue: Byte;
    Duration: Cardinal;
  end;

  TTransitions = class(TQueue<TTransition>);

  TByte = class(TObject)
  private
    FBeginValue: Byte;
    FCurrentValue: Byte;
    FEndValue: Byte;
    FDuration: Cardinal;
    FStartTick: Cardinal;
    FTransitions: TTransitions;
    procedure PopTransition;
  public
    procedure AddTransition(ATransition: TTransition);
    constructor Create;
    destructor Destroy; override;
    function HasTransition: Boolean;
    function InTransition: Boolean;
    procedure Recalculate;
    property CurrentValue: Byte read FCurrentValue;
  end;

  TBytes = class(TObjectList<TByte>);

  TByteModulator = class(TObject)
  private
    FItems: TBytes;
    FOnProgress: TNotifyEvent;
    FTimer: TTimer;
    function Finished: Boolean;
    function GetCurrentValue(Index: Integer): Byte;
    function GetItemCount: Integer;
    procedure SetItemCount(Value: Integer);
    procedure Proceed(Sender: TObject);
  protected
    procedure DoProgress;
  public
    procedure AddTransition(Index: Integer; ATransition: TTransition);
    constructor Create;
    destructor Destroy; override;
    property CurrentValues[Index: Integer]: Byte read GetCurrentValue; default;
    property ItemCount: Integer read GetItemCount write SetItemCount;
    property OnProgress: TNotifyEvent read FOnProgress write FOnProgress;
  end;

implementation

{ TByte }

procedure TByte.AddTransition(ATransition: TTransition);
begin
  if ATransition.Duration < 1 then
    ATransition.Duration := 1;
  FTransitions.Enqueue(ATransition);
  Recalculate;
end;

constructor TByte.Create;
begin
  inherited Create;
  FTransitions := TTransitions.Create;
  FDuration := 1;
end;

destructor TByte.Destroy;
begin
  FTransitions.Free;
  inherited Destroy;
end;

function TByte.HasTransition: Boolean;
begin
  Result := FTransitions.Count > 0;
end;

function TByte.InTransition: Boolean;
begin
  Result := FCurrentValue <> FEndValue;
end;

procedure TByte.PopTransition;
var
  Transition: TTransition;
begin
  Transition := FTransitions.Dequeue;
  FBeginValue := FCurrentValue;
  FEndValue := Transition.EndValue;
  FDuration := Transition.Duration;
  FStartTick := GetTickCount;
end;

procedure TByte.Recalculate;
var
  Done: Single;
begin
  Done := (GetTickCount - FStartTick) / FDuration;
  if Done >= 1.0 then
  begin
    FCurrentValue := FEndValue;
    if HasTransition then
      PopTransition;
  end
  else
    FCurrentValue := Round(FBeginValue + (FEndValue - FBeginValue) * Done);
end;

{ TByteModulator }

const
  RefreshFrequency = 25;

procedure TByteModulator.AddTransition(Index: Integer;
  ATransition: TTransition);
begin
  FItems[Index].AddTransition(ATransition);
  FTimer.Enabled := True;
end;

constructor TByteModulator.Create;
begin
  inherited Create;
  FItems := TBytes.Create(True);
  FTimer := TTimer.Create(nil);
  FTimer.Enabled := False;
  FTimer.Interval := MSecsPerSec div RefreshFrequency;
  FTimer.OnTimer := Proceed;
end;

destructor TByteModulator.Destroy;
begin
  FTimer.Free;
  FItems.Free;
  inherited Destroy;
end;

procedure TByteModulator.DoProgress;
begin
  if Assigned(FOnProgress) then
    FOnProgress(Self);
end;

function TByteModulator.Finished: Boolean;
var
  Item: TByte;
begin
  Result := True;
  for Item in FItems do
    if Item.InTransition or Item.HasTransition then
    begin
      Result := False;
      Break;
    end;
end;

function TByteModulator.GetCurrentValue(Index: Integer): Byte;
begin
  Result := FItems[Index].CurrentValue;
end;

function TByteModulator.GetItemCount: Integer;
begin
  Result := FItems.Count;
end;

procedure TByteModulator.Proceed(Sender: TObject);
var
  Item: TByte;
begin
  for Item in FItems do
    Item.Recalculate;
  DoProgress;
  FTimer.Enabled := not Finished;
end;

procedure TByteModulator.SetItemCount(Value: Integer);
var
  I: Integer;
begin
  for I := FItems.Count to Value - 1 do
    FItems.Add(TByte.Create);
  FItems.DeleteRange(Value, FItems.Count - Value);
end;

end.

      

And a small plug-and-play demo (note that only the last prompt is displayed on the shortcuts):

enter image description here

unit Unit2;

interface

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms,
  VCL.ComCtrls, VCL.StdCtrls, Modulation;

type
  TForm2 = class(TForm)
  private
    FBars: array of TProgressBar;
    FLabels: array of TLabel;
    FByteModulator: TByteModulator;
    procedure FormClick(Sender: TObject);
    procedure Progress(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

{ TForm2 }

const
  Count = 10;

constructor TForm2.Create(AOwner: TComponent);
var
  I: Integer;
begin
  inherited Create(AOwner);
  FByteModulator := TByteModulator.Create;
  FByteModulator.ItemCount := Count;
  FByteModulator.OnProgress := Progress;
  SetLength(FBars, Count);
  SetLength(FLabels, Count);
  for I := 0 to Count - 1 do
  begin
    FBars[I] := TProgressBar.Create(Self);
    FBars[I].SetBounds(10, 10 + 30 * I, 250, 25);
    FBars[I].Smooth := True;
    FBars[I].Max := High(Byte);
    FBars[I].Parent := Self;
    FLabels[I] := TLabel.Create(Self);
    FLabels[I].SetBounds(270, 15 + 30 * I, 50, 25);
    FLabels[I].Parent := Self;
  end;
  OnClick := FormClick;
end;

destructor TForm2.Destroy;
begin
  FByteModulator.Free;
  inherited Destroy;
end;

procedure TForm2.FormClick(Sender: TObject);
var
  Transition: TTransition;
  Index: Integer;
begin
  Transition.EndValue := Random(High(Byte) + 1);
  Transition.Duration := Random(3000);
  Index := Random(Count);
  FLabels[Index].Caption := Format('%d > %d @ %f',
    [FByteModulator.CurrentValues[Index], Transition.EndValue,
    Transition.Duration / MSecsPerSec]);
  FByteModulator.AddTransition(Index, Transition);
end;

procedure TForm2.Progress(Sender: TObject);
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    FBars[I].Position := FByteModulator.CurrentValues[I];
end;

initialization
  Randomize;

end.

      

Sukches.

+2


source







All Articles