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), andValue: array of Byte;
(this is the actual values which have disappeared, for example, based onCurrent[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.
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):
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.