123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462(** Animates variables *)(* This is a special case of dynamic variables, where we know that the variable
will be used (and thus, updated) at every iteration of the main loop *)(* If there is an active Avar, the "anim" flag should be set so that the main
loop does not wait for events *)(* We could probably do the same thing using standard Dynvar, and emitting
another event at each iteration. This would be less efficient (?) *)openB_utilsmoduleTime=B_timemoduleMouse=B_mousetypecallback=unit->unittype'at={mutablevalue:'a;(* current value *)mutablestarting_time:Time.toption;mutablefinished:bool;(* this flag is set to true just *before* the computation of the last value *)(* one can create a var with finished = true to behave like a normal var *)mutableframe:int;(* the frame when the value was computed *)mutableprogress:float;(* = float in [0,1] giving the percentage of the animation when the last
v.value was computed. In case of inifinite animation, this is juste the
elapsed Time (in ms). *)init:callback;(* function to be called before the animation starts *)ending:callback;(* function to be called when the animation is finished *)update:'at->float->'a;(* update is a function v --> [0,1] --> 'a which gives the new value of the
variable given the old value, where the interval [0,1] represents the whole
duration of the animation. In case of inifinite animation, it is a function
of the elapsed Time in ms. *)(* TODO: the first argument ('a t) is not currently used. Remove ? *)duration:Time.t;(* a negative duration means an infinite animation *)}(* This global variable keeps track of the number of animations that are not
finished. At this point, this is only for debugging. We cannot rely on it for
programming, because if an animation was started on a layout that is not used
anymore, it will never "finish". Moreover some animations can belong to layouts
that are still alive but hidden (maybe clipped, maybe in a hidden window): in
this case they should not be considered "alive" by the renderer. *)(* not used *)letalive_animations=ref0(* For the moment, in order to indicate that a var is changed, in case it is not
detected by Bogue.has_amin, one should use: Trigger.push_var_changed *)(** this global variable counts the number of frames displayed *)(* of course it should be increased by the main loop *)letframe=ref0letnew_frame()=incrframeletnop()=()letfail__=failwith"This variable does not know how to update itself"letcreate?(duration=1000)?(init=nop)?(ending=nop)?(finished=false)?(update=fail)value={value;starting_time=None;finished;frame=!frame;progress=0.;init;ending;update;duration}letconstantx__=x(** Simulate a mutable normal variable with a fixed value. The value can be
changed by changing v.value.
<OLD>But it cannot update itself: thus v.finished
should never be set to false. </OLD> *)(* in fact, one could use update v u = v.value *)(* this would solve the problem that setting v.value directly doesn't trigger
has_anim, and thus can become unnoticed by the main event loop *)letvarvalue=letupdatev_=v.valueincreate~finished:true~duration:0~updatevalue(** create a fixed value. Behaves a bit like var, with important differences: it
is declared as a new animation (and thus reports "has_anim"), it will always
have this value when initialized (even if v.value was manually changed) and
if v.finished is set to false, the initial value will be set again *)letfixedvalue=create~duration:0~update:(constantvalue)value(* one could use this instead of the global variable
alive_animations *)lethas_animv=notv.finishedletfinishedv=v.finishedletstartedv=v.starting_time<>None(* this should not be called directly (done by get v) *)(* in particular, it assumes that v.finished = false *)letstartv=if!debugthenassert(notv.finished);v.init();lett=Time.now()inincralive_animations;printddebug_event"New animation started. Total=%d"!alive_animations;v.starting_time<-Somet;tletprogressv=v.progressletin_progressv=v.starting_time<>None&¬v.finishedletelapsedv=ifv.duration<0thenroundv.progresselseround(v.progress*.(floatv.duration))(** return the final value, or the current value if v was stopped. This does not
stop the animation and does not trigger 'ending' *)letfinal_valuev=ifv.finishedthenv.valueelseifv.duration<0thenbeginprintddebug_error"Cannot compute the final value for an infinite animation !";v.valueendelsev.updatev1.(** stop the animation, but doesn't change the value *)(* can be called directly *)letstopv=ifv.finishedthen()elsebeginv.ending();v.finished<-true;decralive_animations;printddebug_event"Animation finished. Total remaining=%d"!alive_animationsend(** finish the animation and set the value to the expected final value *)letfinishv=letfinal=final_valuevinstopv;v.value<-final(* reset so that the animation will start again *)letresetv=ifv.finishedthenprintddebug_warning"Resetting animation."elseifv.starting_time<>Nonethenbeginprintddebug_warning"Animation was reset before ending.";decralive_animationsend;v.starting_time<-None;v.finished<-false;v.frame<-!frame;v.progress<-0.(** start the animation and compute the current value of the variable *)letgetv=ifv.finished||(startedv&&v.frame=!frame)thenv.valueelseletu=(* the rescaled time from 0. to 1. *)lett=Time.now()inlett0=matchv.starting_timewith|Somet0->t0|None->startvinifv.duration<0thenfloatt(* no rescale in this case: infinite animation! *)elseifTime.(t-t0>=v.duration)then(stopv;1.)else(* here v.duration should not be 0 *)Time.(float(t-t0)/.(floatv.duration))in(* we compute the new value: *)letx=v.updatevuinv.value<-x;v.frame<-!frame;v.progress<-u;x(** get the old value. This is the way to get the value if one doesn't want to
start the animation, or if one doesn't want to make any calculation *)letoldv=v.value(** sets the value *)(* if there is an anim running, this has (almost) no effect, since the new value
will be computed anyway. v.progress is *not* modified *)letsetvvalue=v.value<-value;v.frame<-!frame(** create a new Avar by composing with f; the old Avar is still active *)(* this doesn't start the animation *)(* TODO if this one stop just a msec before the old one, and the old one is only
active through the new one, then the old one will never "stop"... but maybe
it is not a problem ? *)letapply_oldfv=letvalue=f(oldv)inletupdate__=f(getv)inletav=create~duration:(v.duration-20)~finished:v.finished~updatevalueinav.starting_time<-v.starting_time;av.progress<-v.progress;ifnotv.finished&&startedvthen(incralive_animations;printddebug_event"New composite animation started. Total=%d"!alive_animations;);avletapplyfv=letvalue=f(oldv)inletupdate__=f(getv)inletduration=v.duration-(elapsedv)increate~duration~finished:v.finished~updatevaluetypedirection=|No|Left|Right|Top|Bottom|TopLeft|TopRight|BottomLeft|BottomRight|Randomletslowdown_oldu=(* between 0 and 1, with speed from 1.8 to 0 *)2.*.(sin((1.+.2.*.u)*.pi/.6.)-.0.5)letslowdownu=(* between 0 and 1, with speed from 2 to 0 *)u*.(2.-.u)letfminab:float=minab(* for 0 to 1 with prescribed initial speed *)letinitial_slope~slope=letu1=2./.slopeinifslope>=2.thenfunu->(* this one is constant = 1 for u >= 2/slope *)ifu<u1then(slope*.u*.(1.-.slope*.u/.4.))else1.elsefunu->u*.(slope+.(1.-.slope)*.u)(* from x1 to x2 with given initial and final slopes *)letinterpol3~slope1~slope2x1x2u=letdx=x2-.x1inx1+.u*.(slope1+.u*.(3.*.dx-.2.*.slope1-.slope2+.u*.(slope1+.slope2-.2.*.dx)))(* from x1 to x2 *)letaffinex1x2u=x1*.(1.-.u)+.x2*.u(* from 0 to x *)letlinearxu=x*.uletreverseu=1.-.uletconcat?(weight=0.5)g1g2=assert(weight>=0.&&weight<=1.);ifweight=0.theng2elseifweight=1.theng1elsefunu->ifu<weighttheng1(u/.weight)elseg2((u-.weight)/.(1.-.weight))(******** examples of animated variables *********)(** create a (slowdowned) integer Avar from x1 to x2 *)letfromto_old?(duration=300)x1x2=letupdate_u=lett=slowdownuinround(floatx1*.(1.-.t)+.floatx2*.t)increate~duration~updatex1(** create a (slowdowned) integer Avar from x1 to x2 *)letfromto?(duration=300)?endingx1x2=ifx1=x2thenfixedx1elseletupdate_u=initial_slope~slope:1.2u|>affine(floatx1)(floatx2)|>roundincreate~duration~update?endingx1letfromto_unif?(duration=300)?endingx1x2=ifx1=x2thenfixedx1elseletupdate_u=affine(floatx1)(floatx2)u|>roundincreate~duration~update?endingx1letfromto_float?(duration=300)?endingx1x2=ifx1=x2thenfixedx1elseletupdate_u=initial_slope~slope:1.2u|>affinex1x2increate~duration~update?endingx1(** piecevise linear, with 2 pieces *)letpl2?(duration=300)~viax1x3=let(weight,x2)=viainifx1=x2&&x2=x3thenfixedx1elseletg1=affine(floatx1)(floatx2)inletg2=affine(floatx2)(floatx3)inletupdate_u=concat~weightg1g2u|>roundincreate~duration~updatex1(** oscillate around the initial position *)letoscillate?(duration=10000)?(frequency=5.)amplitudex0=letf=frequency*.2.*.piinletupdate_u=x0+round(floatamplitude*.(sin(f*.u)))increate~duration~update0(** linear slide-in animation *)letslide_in?(from=Right)?duration~pos~size()=letw,h=sizeinletx0,y0=posinletdx,dy=matchfromwith|No->0,0|Top->0,-h|Bottom->0,h|Right->w,0|Left->-w,0|TopLeft->-w,-h|TopRight->w,-h|BottomLeft->-w,h|BottomRight->w,h|Random->lett=Random.float(2.*.pi)inround(floatw*.cost),round(floath*.sint)inletx=fromto?duration(x0+dx)x0inlety=fromto?duration(y0+dy)y0in(x,y)(** hoffset animation from h1 to h2 *)(* for fun, one could use 'apply' instead to compose several Dynvar, but
certainly less optimized... *)letshow?(duration=300)?init?endingh1h2=letupdate_u=slowdownu|>affine(floath1)(floath2)|>roundincreate~duration~update?init?endingh1lethide?(duration=300)?init?endingh1h2=letupdate_u=reverseu|>slowdown|>affine(floath1)(floath2)|>roundincreate~duration~update?init?endingh1(** fade_in animation *)(* same as fade_out, but accell curve is reversed *)letfade_in?(duration=300)?(from_alpha=0.)?(to_alpha=1.)()=letupdate_u=reverseu|>slowdown|>affineto_alphafrom_alphaincreate~duration~updatefrom_alpha(** fade_out animation *)letfade_out?ending?(duration=300)?(from_alpha=1.)?(to_alpha=0.)()=letupdate_u=slowdownu|>affinefrom_alphato_alphaincreate~duration~update?endingfrom_alpha(* (\** mouse position relative to starting position *\)
* let mouse_motion_x_old ?(threshold=7) window =
* let resist = ref true in
* let x0 = ref 0 in
* let init () = x0 := fst (Mouse.window_pos window) in
* let update _ u =
* let x = fst (Mouse.window_pos window) in
* if !resist then begin
* if abs (x - !x0) > threshold then resist := false;
* 0
* end
* else x - !x0 in
* create ~duration:(-1) ~init ~update 0;; *)(* 'resist threshold' creates a function which, if x stays close to 0 then
returns 0 otherwise returns x (even if later x come back close to 0 ) *)letresistthreshold=letresist=reftrueinfunx->if!resistthenbeginifabsx>thresholdthenresist:=false;0endelsex(* mouse x position relative to starting position *)letmouse_motion_x_old?(threshold=7)?(dx=0)window=letresist=resistthresholdinletx0=ref0inletinit()=x0:=fst(Mouse.window_pos(Lazy.forcewindow))inletupdate__=resist(fst(Mouse.window_pos(Lazy.forcewindow))-!x0)|>(+)dxincreate~duration:(-1)~init~updatedx(* mouse y position relative to starting position *)letmouse_motion_y_old?(threshold=7)?(dy=0)window=letresist=resistthresholdinlety0=ref0inletinit()=y0:=snd(Mouse.window_pos(Lazy.forcewindow))inletupdate__=resist(snd(Mouse.window_pos(Lazy.forcewindow))-!y0)|>(+)dyincreate~duration:(-1)~init~updatedy(** create a new avar from the current position to x2 with C^1 glue *)(* warning: this is not guaranteed to stay between x1 and x2 *)letextendto~durationvx2=letx1=v.valueinifv.finished||not(startedv)thenfromto~durationx1x2elseletslope1=(* we compute the slope at the current point of v *)(* if v has a different duration, the slope (in terms of u) has to be
rescaled *)(* it is difficult to compute the slope this way, since v.update has
integer values; this is why we take du=0.1 quite large. It would be
better to have a 'float update' *)letdu=0.1inletu1=v.progressinletdx=ifu1<duthenv.updatev(u1+.du)-x1elsex1-(v.updatev(u1-.du))inifduration>=0&&v.duration>=0then(float(v.duration*dx))/.(du*.floatduration)elseifduration<0&&v.duration>=0then(floatdx)/.(du*.floatduration)elseifduration>=0then(float(v.duration*dx))/.duelse(* both durations are negative *)(floatdx)/.duinletupdate_u=interpol3~slope1~slope2:(0.)(floatx1)(floatx2)u|>roundin(* print_endline ("SLOPE=" ^ (string_of_float slope1)); *)(* DEBUG *)create~duration~updatex1