Skip to content

Create pages for animation #193

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

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions TOC.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
-: qot
-: skins
-: editor
-: animation
-: ranking
-: multiplayer
-: tournament
Expand Down Expand Up @@ -62,6 +63,20 @@
-: editor_keybinds
-: plugins

[animation]
-: README
-: examples
-: state_machines
-: stage
-: ui
-: layer
-: skin
-: timeline
-: properties
-: tween
-: keyframes
-: event_system

[api]
-: README
-: users
Expand Down
4 changes: 4 additions & 0 deletions en-US/animation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
name: Animation
---
Animations allow you to make cool effects to maps. In Quaver, you can use [Lua](https://www.lua.org/) scripts to make animations, add effects, and more!
127 changes: 127 additions & 0 deletions en-US/animation/event_system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: Event System
---

# Event System

The event system can help you with the following:

* Listen to a specific type of event
* Actively emit an event
* Add custom events

## Event types

How are events classified? Every event emitted has an `EventType` attached to it. Under the hood, the `EventType` is a bitfield that specifies both the category and the specific type of the event:

```
|------------------------------| |------------------------------|
63 32 31 0
Bit Flags for Category Specific Type (Integer)
```

Here are all the event types you can use. You can use them by e.g. `EventType.NoteEntry`.
| Event Type | Is Category | Arguments |
|---|---|---|
| None | No |
| Custom | Yes |
| Note | Yes |
| NoteEntry | No | `HitObject`
| NoteLeave | No |`HitObject`
| NoteDead | No |`HitObject`
| NoteDestroy | No |`HitObject`
| Input | Yes |
| InputKeyPress | No | `HitObject: HitObject, Time: int, Judgement: Judgement`
| InputKeyRelease | No | `HitObject: HitObject, Time: int, Judgement: Judgement`
| Function | Yes |
| FunctionCall | No | `Closure: Function, Arguments: Any[]`
| StateMachine | Yes |
| StateMachineTransition | No | **Not functional for now**
| Timeline | Yes |
| TimelineAddSegment | No | `Segment`
| TimelineRemoveSegment | No |`Segment`
| TimelineUpdateSegment | No |`Segment`
| TimelineTrigger | No |
| TimelineAddTrigger | No |`Trigger`
| TimelineRemoveTrigger | No | `Trigger`
| TimelineUpdateTrigger | No | `Trigger`


When `Is Category` is Yes, this means the `Specific Type` (bits 15 to 0) is 0. This specifies that the entire category is of interest, instead of a specific type.

`EventType.Custom` is a section where you get to decide what each `Specific Type` does.

You can construct an event type with a specific type with the `..` operator:
```lua
-- Constructs a Custom event of specific type 3
eventType = EventType.Custom .. 3
```

You can get the category and the specific type using `GetSpecificType()` and `GetCategory()`. You can also use `ToFriendlyString()` to get a more readable representation of the event type.

```lua
t = EventType.Custom .. 5
print(t.GetSpecificType()) -- 5
print(t.GetCategory().ToFriendlyString()) -- Custom
print(t.ToFriendlyString()) -- Custom[5]
```

## Listening to events

You can listen to an event with `Event.Subscribe`.
For example, the following prints out a message whenever a note appears:

```lua
function onNoteEntry(args)
local hitObject = args.HitObject
print("Note entered at " .. hitObject.StartTime)
end

Events.Subscribe(EventType.NoteEntry, onNoteEntry)
```

You can see, from the example, what an **event listener** looks like: a function with a single parameter `args`.

`args` varies in type, depending on the type of the event. You can always get the type of it using `args.EventType` the varying field is shown in the table above.

To unsubscribe, use `Events.Unsubscribe`:
```lua
Events.Unsubscribe(EventType.NoteEntry, onNoteEntry)
```

## Emitting an event

You can actively emit an event using `Event.Enqueue`.
This will trigger all the functions that are subscribed to this event type.
Call the function, pass the `EventType` as the first argument, and any number of trailing arguments you will use to construct the event arguments (Specified in the table above):

```lua
function custom(args)
print("I received " .. args.Arguments[1] .. " and " .. args.Arguments[2] .. "!")
end
Events.Subscribe(EventType.Custom .. 1, custom)
-- Emits a custom event of specific type 2, with argument 3, "Hi"
Events.Enqueue(EventType.Custom .. 1, 3, "Hi")
-- End of update cycle: "I received 3 and Hi!"
```

**NOTE** `Event.Enqueue()` calls will not be executed right away: they are added to the end of a queue. All the events in this queue will be processed, *in order*, at the very end of each update cycle. So, **do** expect the following:

```lua
a = 1
function custom1(_)
a = 2
end
function custom2(_)
print(a) -- 2!
end
Events.Subscribe(EventType.Custom .. 1, custom1)
Events.Subscribe(EventType.Custom .. 2, custom2)
Events.Enqueue(EventType.Custom .. 1)
Events.Enqueue(EventType.Custom .. 2)
print(a) -- 1!
```



In the future, state machines and all functions should be able to directly hook into the event system.
68 changes: 68 additions & 0 deletions en-US/animation/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: Examples
---

## Swap lanes randomly when any key is pressed
```lua
-- Cache receptor positions
ReceptorPositions = Stage.GetReceptorPositions()
-- A boolean array where LaneSwapOccupied[i] indicates whether the ith receptor is already in animation
-- This is needed if we want to make animations less 'messy'
LaneSwapOccupied = {}
for i = 1, 7 do
table.insert(LaneSwapOccupied, false) -- Pre-initialize with false
end
-- The number of occupied lanes
FreeLaneCount = 7
-- The duration of a lane swap animation
ANIMATION_DURATION = 500

-- Event listener for Events.InputKeyPress event
function onPress(args)
-- Retrieve information passed from args
local info = args.hitObject
local pressTime = args.time
local judgement = args.judgement
local lane = info.lane
-- The lane is already in a swap animation, or there is less than 2 lanes for us to swap
if LaneSwapOccupied[lane] or FreeLaneCount < 2 then
return -- skip everything below
end
-- Mark the lane as occupied
LaneSwapOccupied[lane] = true;
-- Generate a random lane to swap with
local anotherLane = lane
while LaneSwapOccupied[anotherLane] do
anotherLane = math.random(1, Map.KeyCount)
end
-- Mark this other lane occupied as well
LaneSwapOccupied[anotherLane] = true;
FreeLaneCount = FreeLaneCount - 2

-- Set tween segments to smoothly swap the X values of the two lanes
local easingWrapper = EasingWrapper.From(Easing.InQuad)
Timeline.Add(Segment(pressTime, pressTime + ANIMATION_DURATION,
Timeline.Tween(ReceptorPositions[lane][1], ReceptorPositions[anotherLane][1], Tween.X(Stage.Receptor(lane)), easingWrapper),
true))
Timeline.Add(Segment(
pressTime, pressTime + ANIMATION_DURATION,
Timeline.Tween(ReceptorPositions[anotherLane][1], ReceptorPositions[lane][1], Tween.X(Stage.Receptor(anotherLane)), easingWrapper),
true))

-- We want to mark the lanes 'unoccupied' after the animation finishes
Timeline.Add(Trigger(pressTime + ANIMATION_DURATION,
Timeline.CustomTrigger(function()
LaneSwapOccupied[lane] = false
LaneSwapOccupied[anotherLane] = false
FreeLaneCount = FreeLaneCount + 2
end), true))

-- Swap the positions in cache
tmp = ReceptorPositions[lane]
ReceptorPositions[lane] = ReceptorPositions[anotherLane]
ReceptorPositions[anotherLane] = tmp
end

-- onPress will be called when a key is pressed
Events.Subscribe(EventType.InputKeyPress, onPress)
```
67 changes: 67 additions & 0 deletions en-US/animation/keyframes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: Keyframes
---

# Keyframes

The keyframes payload allows you to make a sequence of interpolations to a property, bounded by a single setter.

You can construct a `Keyframes` payload from any property:

`property.Keyframes(keyframes: Keyframe[])`

A `Keyframe` describes a value that your specified property should have at the specified *relative* time, and how easing is performed between the current keyframe and the next. You can construct it with `{time, value, [easing]}`. For example:

```lua
-- X value of the receptor
r2x = Stage.GetLanePositions()[2][1]
Timeline.Add(10000, 11500,
Stage.LaneContainer(2).XProp.Keyframes(
{
{ 0, r2x },
{ 500, r2x + 100 },
{ 1000, r2x },
{ 1500, r2x - 100, "InCirc" },
{ 2000, r2x, "OutCirc" }
}
)
)

```

Specifies that we want to change the X value of the 2nd receptor as follows:
* Start at the same position as before at time t=10000
* Move to 100 pixels rightwards at t=10500
* Move back to the original X at t=11000
* Move to 100 pixels leftwards at t=11500, with `InCirc` easing
* Move back to the original X at t=12000, with `OutCirc` easing

The third parameter can be omitted. In this case, it would be assumed that the transition is linear (using `EasingFunctions.Linear`). For more information on what you can put here, see [EasingFunction, Tweens](/docs/animation/tween#easingfunction)

The `time` in the constructor is relative and scaled, as mentioned before. The first keyframe should always be at time `t=0`. What only matters here is the **proportion**. In this case every transition takes up a quarter of the overall time equally. The actual length of each transition is determined by this proportion and the start and end time of the segment (`10000` and `12000` in this case).

Precisely, if the last keyframe has relative time `tz`, the transition between two keyframes at relative time `ti` and `tj` in a segment with start time `startTime` and end time `endTime` takes up `(endTime - startTime) * (tj - ti) / tz` amount of time, starting at time `startTime + (endTime - startTime) * ti / tz`.

This means the keyframes can be scaled easily if you just manipulate the `endTime` value. For example, setting `endTime` to `14000` makes the transition twice as long:
* Start at the same position as before at time t=10000
* Move to 100 pixels rightwards at t=11000
* Move back to the original X at t=12000
* Move to 100 pixels leftwards at t=13000, with `InCirc` easing
* Move back to the original X at t=14000, with `OutCirc` easing

You are allowed to transition vectors as well. For example, position:
```lua
-- Position of the first receptor (lane 1)
pos = Stage.GetLanePositions()[1]
Timeline.Add(10000, 11500,
Stage.LaneContainer(1).PositionProp.Keyframes(
{
{ 0, { pos[1], pos[2] } },
{ 500, { pos[1] + 100, pos[2] - 100 } },
{ 1000, { pos[1], pos[2] - 100 } },
{ 1500, { pos[1] - 100, pos[2] - 50 }, "InCirc" },
{ 2000, { pos[1], pos[2] }, "OutCirc" }
}
)
)
```
68 changes: 68 additions & 0 deletions en-US/animation/layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: Layer
---

# Layer

The gameplay field has objects drawn in ordered layers. Layers can be accessed by `Layers[name]`, such as `Layers["Foreground"]`.

You can set a drawable's layer with its `Layer` field, or by `drawable.WithLayer(layer)`. On this occasion, a string can be used as a shorthand to `Layers[name]`:

```lua
sprite = New.Sprite("pixel art.jpg")
.Resize({200, 200})
.WithParent(Stage.PlayfieldContainer)
-- .WithLayer("Foreground")
.Align("MidCenter")
-- sprite.Layer = Layers["Foreground"]
-- Shorthand:
sprite.Layer = "Foreground"
```

You can create a layer with `New.Layer(name: string)`:
```lua
customLayer = New.Layer("Custom")
```

Created layers need constraints to tell them to draw between which layers. This is done by the following ways:

`layer.RequireAbove(lowerLayer)` puts the layer above `lowerLayer`.

`layer.RequireBelow(upperLayer)` puts the layer below `upperLayer`

`layer.RequireBetween(lowerLayer, upperLayer)` puts the layer between the two layers

If you want to arrange multiple layers at once, use `Layers.RequireOrder(layerOrder: Layer[])`:

```lua
customLayer = New.Layer("Custom")
Layers.RequireOrder({
"HitObjects",
customLayer,
"Background"
})
```

This specifies that `HitObjects` is above `Custom`, and `Custom` is above `Background`.

The order should be arranged top to bottom, with top being the first.

Again, we can use string shorthand to address layers.

If you use `Layers.Dump()`, you can see which layers are present in the game's `runtime.log`:

```
RUNTIME - DEBUG: 11 Layers:
RUNTIME - DEBUG: Layer 'Top' (0): 0 drawables
RUNTIME - DEBUG: Layer 'Foreground' (1): 1 drawables Container
RUNTIME - DEBUG: Layer 'Hits' (2): 7 drawables Container, Container, Container, Container, Container, Container, Container
RUNTIME - DEBUG: Layer 'HitObjects' (3): 7 drawables Container, Container, Container, Container, Container, Container, Container
RUNTIME - DEBUG: Layer 'TimingLines' (4): 1 drawables Container
RUNTIME - DEBUG: Layer 'HitPositionOverlays' (5): 0 drawables
RUNTIME - DEBUG: Layer 'ReceptorsAndLighting' (6): 14 drawables
RUNTIME - DEBUG: Layer 'Background' (8): 1 drawables Container
RUNTIME - DEBUG: Layer 'Default' (9): 0 drawables
RUNTIME - DEBUG: Layer 'Bottom' (10): 0 drawables
```

The `Top` and `Bottom` layer cannot have any children attached to it. Layers cannot be put on top of `Top`, nor below `Bottom`. They mark the very top and bottom of the drawing. `Foreground` contains all the UI elements, such as combos, rating display, etc.
Loading