Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Animation DSL - Initial commit #5062

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

GalenIvanov
Copy link

Animate is an experimental animation dialect. It's main goal is to provide the programmers with an easy declarartive way to describe simple animation as an extenson to Draw. It also provides mechanism for animating arbitrary word! or path! values.

@hiiamboris
Copy link
Collaborator

FYI team uses a set of prefixes to classify commits

@greggirwin greggirwin changed the title Animation DSL - Initial commit FEAT: Animation DSL - Initial commit Feb 5, 2022
@greggirwin
Copy link
Contributor

I added FEAT to the description.

Thanks @GalenIvanov !

@hiiamboris
Copy link
Collaborator

Thanks for PR and especially the docs.

I haven't gotten to the effects part yet, so preliminary thoughts... I can see there's a lot of work ahead.

  1. It is generally your goal as a contributor to figure out how your code fits into the big picture: namespace where it will live, how is it loaded, how is it used, and how does it interact with other pieces (View, VID, event system). We'll advice, but right now it's totally a mess :)

  2. I can see that draw and animate are mutually exclusive. on-created [animate [..] face] is not a design, it's a hack. I propose view [base draw animate [...]]. This way we are explicit that draw facet is occupied and don't have to deal with it in any other way. Let's also liberate animate from the face parameter. Let it work upon arbitrary blocks, then user can decide what to do with the blocks. VID is expected to be magical, so it should be.

  3. Currently animate overrides all actors of the face, which is unacceptable. I propose using single global event handler and not interfere with the actors.

  4. Ideally we need some global timer for this, but it's not in the View design, so for now let's just note and defer it. Let's make it clear in the docs that rate facet should be set for at least one face for animation to work.

  5. Would be nice if animate told me if I made a syntactic error, e.g. animate [start 1.0 duration 3.0 two-way] face
    It also silently accepts animate [start 1.0 duration 3.0 pen blue line 0x0 from 10x10 to 70x50 loop two-way] face but loop doesn't kick in (because it's in the wrong place). I expected an error.

  6. Why not make it orderless where possible? The biggest cognitive load for me in this dialect is the order of parameters in each scope. Ease then loop or loop then ease? What logic should I employ to decide? And without error messages I'm left in the blind as to what's going on. My point here is that with the ordered approach I will have to look up the docs all the time, because the order seems arbitrary to me.

  7. This code: view [base rate 60 on-create [animate [error] face]] currently crashes the console with a message *** Internal Error: circular reference not allowed. I reported it, but why animate produces circular references in the first place? I think it shouldn't.

  8. Easing functions should not be global. I propose accepting lit-words for predefined functions, and function literals for user-defined. ease in-circ & ease :my-func.

  9. All time values (like duration) should accept [time! float! integer!] but not percent!. Let's properly use Red type richness.

  10. I think ease should also be possible to override on parameter level (rather than a frame).

  11. Example shows ref: start 2.0 duration 5.0. Draw already uses set-word syntax. How do they work together here, animate and draw?

  12. Can <ref> refer to animations that will be declared later? Should it be supported?

  13. This doesn't work. Bug?

    view [
     	base white rate 60 on-create [
     		animate [
     			ref: start 0
     			start when ref starts duration 3 delay 1 ease :ease-in-expo loop two-way forever
     				box 10x10 from 10x70 to 70x70
     				box 12x10 from 12x70 to 72x70
     		] face
     	]
    ]
    

Minor things:

  • Easing functions should have plots in the documentation, like on that website.
  • Let's use lowercase file name. Some FS are case sensitive.
  • Loops: what about once, twice aliases for 1 times & 2 times?
  • Should there be an option to mirror the easing function in even iterations of two-way animations? Maybe bounce as a variant of two-way?
  • I noted the time glitch in the animation room. Deserves further investigation.
  • Idea for the future: we could use motion blur (when we have it) for quickly moving objects like they do in cinema.

@hiiamboris
Copy link
Collaborator

hiiamboris commented Feb 6, 2022

  1. For some reason I can't make this draw me a shape (only the box is drawn):
Red []

#include %animate.red

view compose/deep/only [
	base 80x80 white rate 60 on-create [
		animate [
			start 0 duration 3 ease :ease-in-expo
				[box 10x10 from 10x70 to 70x70]
				shape [move 40x20 'line from -30x40 to 30x40 from 60x0 to -60x0 close]
		] face
	]
]

I then tried to prerender it:

img: draw 80x80 [shape [move 40x20 'line -30x40 60x0 close]]
view compose/deep/only [
	base 80x80 white rate 60 on-create [
		animate [
			start 0 duration 3 ease :ease-in-expo
				[box 10x10 from 10x70 to 70x70]
				[image from 0x0 to 80x0 img]
		] face
	]
]

But that fails with

*** Script Error: from has no value
*** Where: get
*** Near : img-d
*** Stack: view show show do-safe on-create animate 

@GalenIvanov
Copy link
Author

GalenIvanov commented Feb 6, 2022

@hiiamboris Regarding 14 - apparently I have forgotten about lit-words in shape. This is easily fixed.

@hiiamboris
Copy link
Collaborator

Why should animate even know Draw syntax? Why not just skip it?

@GalenIvanov
Copy link
Author

Why should animate even know Draw syntax? Why not just skip it?

It needs to know some of the Draw commands, due to the subpixel precision (automatic 10x upscale ot the lienar values and scale 0.1 0.1). For example image img will display the image ten times smaller than its original size. There are similat issues with transformations of pen anf fill-pen.

@hiiamboris
Copy link
Collaborator

I see. What if it scaled all pairs and some selected numbers like line-width and circle radii, and skipped everything else?

@GalenIvanov
Copy link
Author

Yes, it's possible to look only for these special cases. But it still has to keep track of words (Draw commands) without knowing their meaning, for the tween targets.

@hiiamboris
Copy link
Collaborator

Isn't from & to enough to identify tween targets?

@pekr
Copy link

pekr commented Feb 7, 2022

I like the idea of important options being exposed at VID level directly, e.g: view [base 300x100 draw [...] effect [...] animate [...]].

I also think, that we should use grouping by using blocks, where it makes sense. In other words - how can I distinguish, how many of the following elements are affected? Is that till the next start sequence?

    start 3.0 delay 0.5 ease :ease-out-back
        text from 670x180 to 70x180 "- Human-friendly syntax"
        text from 670x210 to 70x210 "- Homoiconic"
        text from 670x240 to 70x240 "- Functional, imperative, reactive and symbolic programming"
        text from 670x270 to 70x270 "- Rich set of built-in datatypes (50+)"
        text from 670x300 to 70x300 "- Powerful PEG parser DSL included"
        text from 670x330 to 70x330 "- Cross-platform native GUI system, with a UI DSL and drawing DSL"

@GalenIvanov
Copy link
Author

@pekr Yes, each startstatement affects all the from .. to .. statements until the next start. Btw nothing prevents you from using blocks for the code after start declaration - you can try with the above example:

start 3.0 delay 0.5 ease :ease-out-back [
    text from 670x180 to 70x180 "- Human-friendly syntax"
    text from 670x210 to 70x210 "- Homoiconic"
    text from 670x240 to 70x240 "- Functional, imperative, reactive and symbolic programming"
    text from 670x270 to 70x270 "- Rich set of built-in datatypes (50+)"
    text from 670x300 to 70x300 "- Powerful PEG parser DSL included"
    text from 670x330 to 70x330 "- Cross-platform native GUI system, with a UI DSL and drawing DSL"
]        

@hiiamboris
Copy link
Collaborator

hiiamboris commented Feb 8, 2022

  1. scale errors are not tolerated :)
    (I forgot to specify the 2nd scale parameter)
view [
	base 300x300 white rate 60 on-create [
		animate [
			start 0 duration 60
				scale from 1.0 to 0.0
		] face
	]
]
*** Script Error: invalid Draw dialect input at: [scale1: scale 10.0]
*** Where: ???
*** Near : none
*** Script Error: scale1 is unset in path scale1/2
*** Where: set
*** Near : val
*** Stack: view do-events do-actor do-safe process-timeline tween
*** Script Error: scale1 is unset in path scale1/2
*** Where: set
*** Near : val
*** Stack: do-actor do-safe process-timeline tween
*** Script Error: invalid Draw dialect input at: [scale1: scale 10.0]
*** Where: ???
*** Near : none
*** Script Error: scale1 is unset in path scale1/2
*** Where: set
*** Near : val
*** Stack: do-actor do-safe process-timeline tween
*** Internal Error: circular reference not allowed
*** Where: body
*** Near : none
*** Stack:
(crashed)

@ne1uno
Copy link

ne1uno commented Feb 8, 2022

Text-fx.red demo, window isn't quite wide enough. anim-block: has 720, needs 750 all over.
win10, I think DPI is whatever was default. I usually needed to go up a few percent in past OS.

@hiiamboris
Copy link
Collaborator

  1. Particles do not accept literal block:
view [
	base 300x300 white rate 70 on-create [
		animate [
			start 0 duration 60
				particles wtf-is-this-for? []
		] face
	]
]
*** Script Error: invalid Draw dialect input at: [particles wtf-is-this-for? []]
*** Where: ???
*** Near : none
*** Script Error: invalid Draw dialect input at: [particles wtf-is-this-for? []]
*** Where: ???
*** Near : none
*** Internal Error: circular reference not allowed
*** Where: body
*** Near : none
*** Stack:

@hiiamboris
Copy link
Collaborator

  1. scale itself is not scaled:
view [
	base 300x300 white rate 70 on-create [
		animate [
			start 0 duration 60
				pen blue
				scale from 1.0 to 1.0 from 1.0 to 1.0
				box 1x1 2x2   
				box 1x1 20x20   
		] face
	]
]


(as if 10.0 to 10.0)

@GalenIvanov
Copy link
Author

@ne1uno Yes, DPI - but I think it's a Draw issue.

@GalenIvanov
Copy link
Author

@hiiamboris 15 - the first error is a Draw error. After that the Draw block is cleared and the tween target of scale (scale x) could not be found.
I think I can test the generated Draw block by trying to draw it to an image and return the error if any and exit

@GalenIvanov
Copy link
Author

@hiiamboris Thank you once again for your constructive comments! I think fixed more of the issues:

  1. I enclosed the code in a context, but I definitely need your advice here for more details.
  2. Done - animate now has only one argument – a block of commands and returns a drawing block.
  3. Done - it now uses a global event handler for time
  4. Added text in the docs for the rate facet.
  5. Added error checking for start syntax. Using parameters of start used outside start statement trigger an error.
  6. The order of parameters after start now doesn’t matter.
  7. I can’t reproduce it.
  8. Done: lit-words for predefined functions, and function literals for user-defined.
  9. Fxed. The time values for start, duration and delay now accept [time! float! integer!] but not percent!.
  10. That could be useful – I’ll see to it.
  11. animate keeps set-word! when they precede a Draw command. Set-words before start are not kept in the draw block and are only used as a reference for other animations.
  12. At the moment it’s not possible to refer to animations that will be declared later. The process is a single pass right now. I don’t know if looking ahead for references should be supported – it’s not that important for me.
  13. Fixed – I was only checking for a single start definition followed by draw / animate words. Now they can follow one after another.
  14. Draw errors - Fixed – now lit-word! s in shape block are properly collected.
  15. Added error check – before animate returns the processed draw block, it tries to render the block to an image and triggers an error if needed.
  16. particles now accept block! for a prototype and not only word!
  17. Scale - Fixed

@hiiamboris
Copy link
Collaborator

Great, thanks!!

Seeing that like Bolek you like the manual labor of specifying function locals, I run the global words check on simple animation:

Red []

leak-check: function [code] [
	old: values-of system/words
	also do code (
		new: values-of system/words
		repeat i length? words: words-of system/words [
			unless any [
				:old/:i =? :new/:i
				all [									;-- newly loaded words
					i > length? old
					unset? :new/:i
				]
				all [									;@@ workaround for #5065
					any-function? :old/:i
					:old/:i = :new/:i
				]
			][
				w: pad words/:i 15
				s1: mold/flat/part :old/:i 30
				s2: mold/flat/part :new/:i 30
				print rejoin ["leak: "w"^-= "s1" -> "s2]
			]
		]
	)
]

leak-check [do %animate.red]
view [
 	base white rate 60 draw leak-check [animate [
 			ref: start 0
 			start when ref starts duration 3 delay 1 ease 'ease-in-expo loop two-way forever
 				box 10x10 from 10x70 to 70x70
 				box 12x10 from 12x70 to 72x70
 	]]
]

Output is:

leak: w              	= unset -> "vline"
leak: drag           	= unset -> func [dir speed][speed: speed 
leak: animate        	= unset -> func [{Takes a block of draw a
leak: tween          	= none -> func [{Interpolates a value be
leak: trace-path     	= none -> func [{Traverse the path's dra
leak: gravity        	= none -> func [dir speed][vx: speed * c
leak: id             	= unset -> [0.0 0.0 0.0 0]
leak: w              	= "vline" -> [72x70]
leak: d              	= unset -> 3
leak: p              	= unset -> [box 12x10 from 12x70 to 72x70
leak: ref            	= unset -> ref
leak: w1             	= unset -> [12x10 from 12x70 to 72x70]
leak: tmp            	= unset -> 120x700
leak: v1             	= unset -> 120x700
leak: v2             	= unset -> 720x700
leak: ani-start      	= unset -> [scale 0.1 0.1 line-width 10 b
leak: p1             	= unset -> [12x70 to 72x70]
leak: p2             	= unset -> [72x70]
leak: dl             	= unset -> 1
leak: trgt           	= unset -> box1/3
leak: w2             	= unset -> []
leak: pen-mark       	= unset -> [box 12x10 from 12x70 to 72x70
leak: img-mark       	= unset -> [box 12x10 from 12x70 to 72x70
leak: scale-mark     	= unset -> [box 12x10 from 12x70 to 72x70
leak: translate-mark 	= unset -> [box 12x10 from 12x70 to 72x70
leak: rotate-mark    	= unset -> [box 12x10 from 12x70 to 72x70
leak: trans-mark     	= unset -> [box 12x10 from 12x70 to 72x70
leak: p-st           	= unset -> [box 12x10 from 12x70 to 72x70
leak: test-img       	= unset -> make image! [100x100 #{0000000
leak: draw-err       	= unset -> make image! [100x100 #{0000000
leak: box0           	= none -> [box 100x100 100x700 box1: box
leak: box1           	= none -> [box 120x100 120x700]

Obviously, animate and tween are welcome here, but the rest... ;)

@hiiamboris
Copy link
Collaborator

Please also optimize tween per my comments above.

@greggirwin
Copy link
Contributor

greggirwin commented Feb 10, 2022

@hiiamboris all the leaks are not function locals though, some are parse rule artifacts.

@greggirwin
Copy link
Contributor

Please also optimize tween per my comments above.

What optimization are you referring to @hiiamboris. I can't tell from finding tween here. Is it requiring only from/to?

Copy link
Collaborator

@hiiamboris hiiamboris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I needed to click submit for comments to be visible.

tween: function [
{Interpolates a value between value1 and value2 at time t
in the stretch start .. start + duration using easing function ease}
target [word! any-path!] {the word or path to set}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target argument is excessive. tween path ... is the same thing as path: tween ....
To be easy on event generation I'm using maybe word: value setter

t [number!] {Current time}
ease [function!] {Easing function}
][
end-t: duration * 1.09 + start ; depends on the easing!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The moment I saw this magic number it occurred to me it's the cause of time-fx glitch when window is moved. All it took is to remove this line and t <= end-t condition.

if 3 = length? val2 [val2: val2 + 0.0.0.0]
val: val1
repeat n length? val1 [
val/:n: to integer! val1/:n + (val2/:n - val1/:n * ease t - start / duration) % 256
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage of % isn't correct here is it? Some tweening functions would exceed the max value and bounce around it, so we should just clip the value between 0 and 255.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH the whole repeat is not needed here, since tuple arithmetic is good enough as it is to interpolate tuples:

t-norm: ease t - start / duration
val: add val1 * (1 - t-norm) val2 * t-norm

Then you don't need to turn 3-tuples into 4-tuples, and you don't need a special case for tuples at all: it works for them and for numbers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiiamboris Thanks for all the comments! I'll go through all of them.

]
][
val: val1 + (val2 - val1 * ease t - start / duration)
if integer? val1 [val: to integer! val]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic to val1 val will be faster than a conditional.

Comment on lines 1687 to 1689
start [number!] {Start of the time period}
duration [number!] {Duration of the time period}
t [number!] {Current time}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of tween calls can be rewritten from tween .. start duration t .. into tween .. t - start / duration .., to make it more general and lessen the number of arguments. Do I have to tie animation to time? What if I'm making an animation editor, and want it to simply spit out a frame based on the slider value (0 to 1)? In this case I would have to invent time only to make it work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want tween to be a global function, so it could be used with arbitrary GUI elements. For that it has to be general.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I'm making an animation editor, and want it to simply spit out a frame based on the slider value (0 to 1)?

This ties directly to both Bret Victor refs and time travel. The general slider/replay concept also applies to event streaming and things like session replay. For those there may be both manual and synthetic time. e.g. playing back a session of API calls and animating against the time that was stored for them.

exit
]

insert-event-func :anim-on-time
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to insert the same function on every animate invocation, eventually blocking the event loop from any useful work and multiplying the amount of work animate performs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiiamboris very good point. I searched R2 code, knowing I had hit this same issue. Almost 800 files contained insert-event-func, with many dupes of course. Wow. Here's what I had in one IOS support file, for a specific event func:

    use [evt-func-installed?] [
        evt-func-installed?: func [fn] [
            found? find system/view/screen-face/feel/event-funcs :fn
        ]

        insert-my-view-event-func: does [
            if not evt-func-installed? :view-evt-func [
            	insert-event-func :view-evt-func
            ]
        ]

        remove-my-view-event-func: does [
            if evt-func-installed? :view-evt-func [
            	remove-event-func :view-evt-func
            ]
        ]
    ]

Other apps just had the core bit directly

    if not find system/view/screen-face/feel/event-funcs :evt-func [
    	insert-event-func :evt-func
    ]


; global event handler
anim-on-time: func [face event][
if event/type = 'time [process-timeline]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is global handler, we should take care to have minimal footprint on performance and ensure we stop when there's nothing to animate. It's unclear to me how it works here, maybe timeline becomes empty and process-timeline exits soon?

@hiiamboris
Copy link
Collaborator

@hiiamboris all the leaks are not function locals though, some are parse rule artifacts.

Possibly. Hard to tell in 2K LOC ;)
red/REP#60 ?

@hiiamboris
Copy link
Collaborator

Almost 800 files contained insert-event-func

I also thought maybe this check should be embedded into insert-event-func instead of relying on programmers common sense?

@greggirwin
Copy link
Contributor

If it's the same func, absolutely. There is no reason to add the same func more than once. It's a mezz so is an easy change. Let's confirm with @dockimbel and @qtxie, so we're all on the same page.

@greggirwin
Copy link
Contributor

That saves @GalenIvanov from having to change it too. :^)

@GalenIvanov
Copy link
Author

GalenIvanov commented Feb 11, 2022

@hiiamboris I optimized tween as per your comments. Thank you very much!
It's now free of target and time parameters.
Regarding the special case I had for tuples - I realize it was due to the fact that I was subtracting the values first and eventually setting them to 0.0.0.0

@hiiamboris
Copy link
Collaborator

So, regarding

1. I enclosed the code in a context, but I definitely need your advice here for more details.

I would have proposed including it from the bottom of view.red, because I'm certain most people are not going to download it to try out. But given recent events we better ask @greggirwin first perhaps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants