mirror of
https://github.com/stevenrobertson/cuburn.git
synced 2025-02-05 11:40:04 -05:00
Start of JSON encoding documentation.
This commit is contained in:
parent
45e3a4c557
commit
e973dcf9d7
@ -1,17 +1,307 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
u"""
|
||||||
|
These types are used to define the document schemas in the `specs` module. The
|
||||||
|
specs are native-Python representations of the schemas, but are designed to be
|
||||||
|
easily transformed into, say, JSON, for use in other languages and
|
||||||
|
applications.
|
||||||
|
|
||||||
|
Each spectype describes the method used by cuburn to encode its value into a
|
||||||
|
plain JSON entity. With one exception (splines), the encoding is
|
||||||
|
straightforward and context-free.
|
||||||
|
|
||||||
|
Specs may change without notice, and are not currently versioned.
|
||||||
|
Implementations should tolerate ambiguity, missing or unknown properties, and
|
||||||
|
improper encodings in whatever way is best for the user.
|
||||||
|
|
||||||
|
# Dict
|
||||||
|
|
||||||
|
The most common spectype is a plain Python dict, and accordingly doesn't have
|
||||||
|
a type defined in this module. Each key must be a string which would be a
|
||||||
|
valid Python identifier; values may be any type below, or a nested dict.
|
||||||
|
There are two keys that hold special significance:
|
||||||
|
|
||||||
|
- "type", if present, shall be an ordinary string, used for identifying the
|
||||||
|
type of a top-level object in situations where that information may not be
|
||||||
|
recoverable from context. If a spec dict has a "type" param, any instances
|
||||||
|
of the spec should have an identical "type" property.
|
||||||
|
|
||||||
|
- "doc", if present, shall be an ordinary string used to describe the dict
|
||||||
|
(in Markdown syntax). This shall be ignored by any instances, and is mostly
|
||||||
|
there for self-describing UIs.
|
||||||
|
|
||||||
|
Instances of spec dicts should be encoded to native JSON dicts.
|
||||||
|
|
||||||
|
Missing values have semantic significance[^missing]. Encoders must omit any
|
||||||
|
key from an encoded dict which a user did not include. Encoders must also
|
||||||
|
recursively omit any key whose child is an empty dict.
|
||||||
|
|
||||||
|
[^missing]: Although direct property access in cuburn through a `use.Wrapper`
|
||||||
|
type will often provide a sensible default value, various container methods —
|
||||||
|
`'x' in y`, `y.keys()` — only include keys that were present on the decoded
|
||||||
|
object. In the case of rendering, the output will be the same, but the process
|
||||||
|
will take longer. In other cases, such as mutation, having default, dummy
|
||||||
|
values present *will* change the result. Plus it just makes things into an
|
||||||
|
unreadable soup to have too many default values in there.
|
||||||
|
|
||||||
|
# Map
|
||||||
|
|
||||||
|
A Map is a dict where possible keys are not known in advance. The spec of each
|
||||||
|
value in the Map is the same, and is given by `Map.type`. This is used, for
|
||||||
|
instance, to implement the `"xforms"` entity.
|
||||||
|
|
||||||
|
Maps should be encoded exactly as plain dicts are above.
|
||||||
|
|
||||||
|
# List
|
||||||
|
|
||||||
|
A List indicates a list of values of a single known type. Encode as a plain
|
||||||
|
JSON list.
|
||||||
|
|
||||||
|
An empty List is semantically distinct from a missing List. (For example, it
|
||||||
|
might be overriding a List with a non-empty default value.) If a user has
|
||||||
|
specified an empty list, it must be present in the encoded entity.
|
||||||
|
|
||||||
|
It's recommended to avoid lists of Scalars or RefScalars, as these can be
|
||||||
|
confused for splines by simple-minded parsers/formatters.
|
||||||
|
|
||||||
|
# Spline
|
||||||
|
|
||||||
|
A cubic spline. The heart of the genome specification, and easily the most
|
||||||
|
complex type, with a lot of exceptions and some context sensitivity.
|
||||||
|
|
||||||
|
## Spectype parameters
|
||||||
|
|
||||||
|
- `default` specifies a default scalar value. This value will be used if no
|
||||||
|
value has been specified. It may also be used during blending (see
|
||||||
|
"Blending" below).
|
||||||
|
|
||||||
|
- `min` and `max` document the minimum and maximum values that a parameter
|
||||||
|
should ever hit during interpolation. These values may be used by
|
||||||
|
higher-level tools, such as UIs, but should not be enforced at the JSON
|
||||||
|
encoding level in any way. May be 'None' to indicate no limit.
|
||||||
|
|
||||||
|
- `interp` indicates the kind of interpolation scaling that should be
|
||||||
|
performed. See "Interpolation scaling" below for details.
|
||||||
|
|
||||||
|
- `period` is the size of the period, such as `360` for a parameter that
|
||||||
|
specifies angles in degrees. For non-periodic parameters, this must be
|
||||||
|
`None`. See "Periodic extension" below.
|
||||||
|
|
||||||
|
- `var` is a boolean property. If True, the value should be interpreted as a
|
||||||
|
variation property. See "Blending" below.
|
||||||
|
|
||||||
|
## JSON encoding and semantics
|
||||||
|
|
||||||
|
Splines make up the overwhelming majority of a given genome's description. I
|
||||||
|
felt it appropriate to optimize for readability and hand-editability at the
|
||||||
|
expense of simplicity of implementation. As a result, splines have a few
|
||||||
|
different ways of being encoded. I know it's icky to overload JSON lists in so
|
||||||
|
many ways, but the parsing complexity this introduces should be very easily
|
||||||
|
contained and the readability benefits are entirely worth it.
|
||||||
|
|
||||||
|
It is recommended that all numbers be encoded using six significant values
|
||||||
|
(i.e. the '%6g' format specifier). This will in most cases eliminate
|
||||||
|
inconsistencies due to single-precision floating-point rounding error.
|
||||||
|
|
||||||
|
### Nodes
|
||||||
|
|
||||||
|
Nodes specify a single point in time across an entire file. Each Spline value
|
||||||
|
in a node specifies the position and velocity of the parameter at that time.
|
||||||
|
(Acceleration is not a free parameter in cubic spline interpolation, and is
|
||||||
|
not specified.)
|
||||||
|
|
||||||
|
If velocity is nonzero at a given point, the parameter value must be expressed
|
||||||
|
as a list of precisely two numbers, with the first being position and the
|
||||||
|
second velocity: `[135.4, -360]`. If velocity is zero, the parameter may be
|
||||||
|
expressed as a single number: `1.3`.
|
||||||
|
|
||||||
|
Implementations must not produce a single-element or empty list; it's either a
|
||||||
|
pair of values in a list, a plain number, or the key should be absent.
|
||||||
|
|
||||||
|
### Edges
|
||||||
|
|
||||||
|
Edges specify parameter values over a continuous period of time, starting from
|
||||||
|
one node and ending at another. The position and velocity of each Spline at
|
||||||
|
the beginning and end of this time are therefore specified by the source and
|
||||||
|
destination nodes, and save for periodic extension (see "Blending" below),
|
||||||
|
these endpoints cannot be altered by the edge.
|
||||||
|
|
||||||
|
Edges can, however, insert knots at any point in time strictly inside the
|
||||||
|
`[0,1]` normalized time over which an edge is defined. Each knot is a `time,
|
||||||
|
position` pair (velocity may not be specified directly). Each pair of values
|
||||||
|
should be concatenated into a single list, such as `[0.5, 1]` or `[0.25, -1,
|
||||||
|
0.75, 3]`.
|
||||||
|
|
||||||
|
As with nodes, empty lists and lists with odd numbers of elements must not be
|
||||||
|
produced during encoding.
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
|
||||||
|
Animations are the blended results produced by merging an edge and its two
|
||||||
|
nodes (each of which in turn may be the result of merging multiple edits).
|
||||||
|
Animations are self-contained and ready to render, so animation Splines
|
||||||
|
contain all the information above.
|
||||||
|
|
||||||
|
There are three representations of a Spline in an animation:
|
||||||
|
|
||||||
|
- Plain number: `1.1`. This indicates the value at both `t=0` and `t=1`, and
|
||||||
|
implies that this value remains constant across the entire animation.
|
||||||
|
|
||||||
|
- Two-element list: `[3.2, 0]`. The first element indicates the value at
|
||||||
|
`t=0`; the second at `t=1`. The velocity at both endpoints is inferred to be
|
||||||
|
zero.
|
||||||
|
|
||||||
|
- Four-or-more-element list: `[45, -360, -135, 0, 0.3, 150]`. This is a list
|
||||||
|
of pairs, and must always contain an even number of elements. The first pair
|
||||||
|
indicates the position and velocity of the parameter at `t=0`; the second,
|
||||||
|
at `t=1`. Subsequent pairs indicate `[time, position]` knots, just as in
|
||||||
|
edges.
|
||||||
|
|
||||||
|
### Merging edits
|
||||||
|
|
||||||
|
Nodes and edges support an "edit stack" (to be documented elsewhere). When
|
||||||
|
merging edits to form a single node, each new Spline value replaces
|
||||||
|
both the position and velocity of any previous Spline value for a given
|
||||||
|
parameter, even if the velocity is an implied 0.
|
||||||
|
|
||||||
|
When merging edge edits, the composed Spline should consist of a list of
|
||||||
|
pairs with no more than one unique value of `time` in the list. If multiple
|
||||||
|
edits include a value for the same `time`, the `position` value for that time
|
||||||
|
should be the one specified on the highest (most recent) edit. To enable
|
||||||
|
removal of lower entries by higher ones, `position` values may also be
|
||||||
|
encoded as JSON `nil` values; these values must be removed from the combined
|
||||||
|
list before passing to the application.
|
||||||
|
|
||||||
|
A simple conceptual procedure for doing this is:
|
||||||
|
|
||||||
|
- Concatenate all Spline lists in all edits, such that the lowest entries are
|
||||||
|
first;
|
||||||
|
- Reverse the list;
|
||||||
|
- For each `time, position` pair starting with the first, remove any subsequent
|
||||||
|
pairs which share the same value of `time`;
|
||||||
|
- For each `time, position` pair starting with the first, remove that pair if
|
||||||
|
the value of `position` is `nil`.
|
||||||
|
|
||||||
|
### Temporal scaling
|
||||||
|
|
||||||
|
When you change the duration of a file, you shouldn't have to go around
|
||||||
|
changing every other value in the file to compensate. And you don't! But the
|
||||||
|
way time is encoded is a bit subtle as a result.
|
||||||
|
|
||||||
|
Cuburn implicitly defines an abstract unit of time (call it U for this
|
||||||
|
section). When rendering, the profile contains a parameter,
|
||||||
|
`profile.duration`, that specifies the duration of 1U in seconds. Combined
|
||||||
|
with `profile.fps`, this defines how Cuburn maps U to individual frames.
|
||||||
|
|
||||||
|
Nodes can define velocities. We always want edges to match up perfectly with
|
||||||
|
the nodes on each side, so that concatenating an edge that has a certain
|
||||||
|
destination node with another edge that has that node as its source will
|
||||||
|
produce a perfectly seamless transition. This means that the velocities in a
|
||||||
|
node must have the same meaning no matter what context. As a result,
|
||||||
|
velocities are defined as the change in position per 1U time.
|
||||||
|
|
||||||
|
Edges, on the other hand, can specify times. If you want to specify that, say,
|
||||||
|
a big spike in a parameter value should happen half-way through the edge, you
|
||||||
|
can specify it by adding a knot at `t=0.5`. Here's the catch: an edge can have
|
||||||
|
an arbitrary duration. The `time.duration` property on an edge specifies how
|
||||||
|
long the edge should be against units U, so `time.duration=3.2` says that this
|
||||||
|
edge should last for 3.2U.
|
||||||
|
|
||||||
|
If we specified knot times directly in U, that means changing the duration
|
||||||
|
from 1 to 3.2 would suddenly put that big spike at `t=0.5` about a sixth of
|
||||||
|
the way through the video, and would leave nothing interesting happening in
|
||||||
|
the latter two-thirds of the video. So instead, knot times are specified
|
||||||
|
relative to the edge duration. (Whenever we use `t`, like in `t=0.5`, we're
|
||||||
|
referring to this file-local unit of time.)
|
||||||
|
|
||||||
|
Ultimately, cuburn (and any other implementations) will take care of this for
|
||||||
|
users, automatically rescaling the velocities to file-local time before it
|
||||||
|
hits the GPU or the UI. The only visible consequence is that scaling duration
|
||||||
|
isn't perfectly linear; the paths taken by splines when you stretch out the
|
||||||
|
video won't be quite the same, because the endcap knots (another internal
|
||||||
|
detail) will change to match the velocity.
|
||||||
|
|
||||||
|
## Blending
|
||||||
|
|
||||||
|
When blending two nodes, the possibility arises that a parameter may be
|
||||||
|
present on one node but not the other. In this case, the missing position will
|
||||||
|
in most circumstances be determined by the value of the `default`
|
||||||
|
parameter of the Spline.
|
||||||
|
|
||||||
|
However, for variations, this is undesirable. Variations in general have no
|
||||||
|
guarantees on parameter stability under interpolation, and interpolating to or
|
||||||
|
from the default value may cause the animation to react violently. Since
|
||||||
|
a variation has no effect as its weight drops to zero, we minimize the chances
|
||||||
|
of these wild interpolations by instead copying a variation parameter's missing
|
||||||
|
position from the opposite node. This behavior is indicated by the `var`
|
||||||
|
property of the Spline; when it is True, the copy-from-other behavior is used
|
||||||
|
during blending.
|
||||||
|
|
||||||
|
There is still the possibility, regardless of `var`, that position information
|
||||||
|
is missing from *both* nodes being blended (because, say, the edge specifies
|
||||||
|
knots for a node missing from both). In this case both sides use the default
|
||||||
|
position value.
|
||||||
|
|
||||||
|
## Periodic extension
|
||||||
|
|
||||||
|
For periodic parameters, adjusting the parameter's position by an integer
|
||||||
|
multiple of the `period` will still result in a signal that perfectly aligns
|
||||||
|
with the node specification. The blend algorithm takes advantage of this to
|
||||||
|
minimize the deviation between the average of the scaled velocities of both
|
||||||
|
nodes and the average velocity of the parameter's path for periodic signals.
|
||||||
|
|
||||||
|
> An example: two nodes both define the `camera.rotation` property as having a
|
||||||
|
> position of 0 and a velocity of -360 (meaning one full counterclockwise
|
||||||
|
> rotation per unit of time U). An edge between these two nodes has a
|
||||||
|
> `blend.duration` value of 3. The average velocity of the two nodes is -360/U,
|
||||||
|
> and the duration is 3U, so the resulting animation has a spline that starts
|
||||||
|
> at 0 and ends at -1080.
|
||||||
|
|
||||||
|
An edge can define additional knots to shape the interior of any Spline path,
|
||||||
|
but in most circumstances may not define any behavior outside of the closed
|
||||||
|
interval `t=(0,1)`; such knots should simply be ignored. However, for Splines
|
||||||
|
that define a `period`, this restriction is expanded to the open interval
|
||||||
|
`t=[0,1]`, to enable per-spline control over the behavior described above. A
|
||||||
|
knot value for a boundary time will be rounded to the nearest value such that
|
||||||
|
the difference between that value and the position specified by the node is an
|
||||||
|
integer multiple of the period size.
|
||||||
|
|
||||||
|
> Continuing the example: let's add a knot for `camera.rotation` at `[1, 10]`
|
||||||
|
> to the edge. Now, the blend will read the knot, round it to the nearest
|
||||||
|
> equivalent value in mod-360 — in this case, just back to 0 — and use that in
|
||||||
|
> place of the -1080 calculated by velocity. The camera will then appear to
|
||||||
|
> rotate counterclockwise for a time, slow to a stop, begin traveling
|
||||||
|
> clockwise for a time, stop again, and finally travel counterclockwise again.
|
||||||
|
"""
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
Map = namedtuple('Map', 'type doc')
|
Map = namedtuple('Map', 'type doc')
|
||||||
map_ = lambda type, d=None: Map(type, d)
|
|
||||||
|
|
||||||
List = namedtuple('List', 'type default doc')
|
List = namedtuple('List', 'type default doc')
|
||||||
list_ = lambda type, default=(), d=None: List(type, default, d)
|
|
||||||
|
|
||||||
Spline = namedtuple('Spline', 'default min max interp period doc var')
|
Spline = namedtuple('Spline', 'default min max interp period doc var')
|
||||||
|
Scalar = namedtuple('Scalar', 'default doc')
|
||||||
|
RefScalar = namedtuple('RefScalar', 'default ref doc')
|
||||||
|
String = namedtuple('String', 'doc')
|
||||||
|
Enum = namedtuple('Enum', 'choices default doc')
|
||||||
|
Palette = namedtuple('Palette', '')
|
||||||
|
|
||||||
|
# Plain helper constructors
|
||||||
|
map_ = lambda type, d=None: Map(type, d)
|
||||||
|
list_ = lambda type, default=(), d=None: List(type, default, d)
|
||||||
|
scalar = lambda default, d=None: Scalar(default, d)
|
||||||
|
refscalar = lambda default, ref, d=None: RefScalar(default, ref, d)
|
||||||
|
|
||||||
|
# Specialized helper constructors
|
||||||
def spline(default=0, min=None, max=None, interp='linear', period=None, d=None):
|
def spline(default=0, min=None, max=None, interp='linear', period=None, d=None):
|
||||||
return Spline(default, min, max, interp, period, d, False)
|
return Spline(default, min, max, interp, period, d, False)
|
||||||
def scalespline(default=1, min=0, max=None, d=None):
|
def scalespline(default=1, min=0, max=None, d=None):
|
||||||
"""Spline helper, with defaults appropriate for a scaling parameter."""
|
"""Spline helper, with defaults appropriate for a scaling parameter."""
|
||||||
return Spline(default, min, None, 'mag', None, d, False)
|
return Spline(default, min, None, 'mag', None, d, False)
|
||||||
|
def enum(choices, default=None, d=None):
|
||||||
|
"""Enum helper. 'choices' is a list or a space-separated string."""
|
||||||
|
if isinstance(choices, basestring):
|
||||||
|
choices = choices.split()
|
||||||
|
return Enum(choices, default, d)
|
||||||
|
|
||||||
class XYPair(dict):
|
class XYPair(dict):
|
||||||
"""
|
"""
|
||||||
@ -21,22 +311,16 @@ class XYPair(dict):
|
|||||||
def __init__(self, type):
|
def __init__(self, type):
|
||||||
self['x'] = self['y'] = self.type = type
|
self['x'] = self['y'] = self.type = type
|
||||||
|
|
||||||
Scalar = namedtuple('Scalar', 'default doc')
|
def export_spec(spec):
|
||||||
scalar = lambda default, d=None: Scalar(default, d)
|
"""
|
||||||
|
Return a JSON-serializable representation of a spec for use in non-Python
|
||||||
# These are scalars, as used in profiles, but which are scaled by some other
|
applications.
|
||||||
# parameter (in the genome) given by name as ``ref``.
|
"""
|
||||||
RefScalar = namedtuple('RefScalar', 'default ref doc')
|
if isinstance(spec, dict):
|
||||||
refscalar = lambda default, ref, d=None: RefScalar(default, ref, d)
|
return dict((k, export_spec(v)) for k, v in spec.items())
|
||||||
|
elif isinstance(spec, basestring):
|
||||||
String = namedtuple('String', 'doc')
|
return spec
|
||||||
def string_(d=None):
|
else:
|
||||||
return String(d)
|
r = spec._asdict()
|
||||||
Enum = namedtuple('Enum', 'choices default doc')
|
r.update(type=type(spec).__name__)
|
||||||
def enum(choices, default=None, d=None):
|
return r
|
||||||
"""Enum helper. 'choices' is a list or a space-separated string."""
|
|
||||||
if isinstance(choices, basestring):
|
|
||||||
choices = choices.split()
|
|
||||||
return Enum(choices, default, d)
|
|
||||||
|
|
||||||
Palette = namedtuple('Palette', '')
|
|
||||||
|
Loading…
Reference in New Issue
Block a user