artc language

...seeks to be a moderately good option for expressing some forms of art concisely and precisely.

What is it?

It's a function, which deterministically evaluates/renders...


  ...from:            to any one of these formats:


┌───────────────┐      ⎧ .usdz files of 3D objects / scenes / animations,⎫
│               │      ⎪                                                 ⎪
│  .artc code   │ -->  ⎨ .json files of 2D Canvas drawing operations,    ⎬
│               │      ⎪                                                 ⎪
│ (Python fork) │      ⎩ .mid  files of MIDI                             ⎭
└───────────────┘


       ⎧ language for,  ⎫    ⎧ OpenUSD,                  ⎫
       ⎪                ⎪    ⎪                           ⎪
It's a ⎨ library for,   ⎬ ── ⎨ CanvasRenderingContext2D, ⎬
       ⎪                ⎪    ⎪                           ⎪
       ⎪ intro to,      ⎪    ⎪ MIDI,                     ⎪
       ⎪                ⎪    ⎪                           ⎪
       ⎩ celebration of ⎭    ⎩ some basic physics        ⎭

It does NOT involve generative AI or seek to replace or automate artistic choices or subtleties.

What's OpenUSD?

What's CanvasRenderingContext2D?

What's MIDI?

Why is it?

It's for:

It doesn't make new stuff possible,
but it tries to make some forms of already-possible stuff somewhat easier.


Language

The language is a fork of Python.

The standard library is very different, and there's some additional syntax / semantics:

Examples
45° -5 m/s² 90 BPMYou can include units in a Quantity literal.
Gray(25%)You can use % to indicate percentages, in many cases.
foo.y = 150cm
foo.y = Same @ 1m30
foo.y += 20cm @ +750ms
foo.y = 0 @ +1s, EaseOut

bar.xform = (
    … @ t1,
    … @ t2,
    …
    … @ t1000,
)
# ^ e.g. via mo-cap as you
#   move a prop through space

You can use the @ BinOp with Assign and AugAssign statements to define keyframes.

You can specify absolute or relative for:

  • Timecodes (via a unary + or - operator)
  • Values (via AugAssign instead of Assign)

The interpolation can be eased with easing functions.

When a relevant assignment statement does not use an @, it's implicitly for timecode zero (@ 0).

shape.x = 1m ± 50cm

... += random( ± 15° )
You can define an Interval via ±, e.g. for implicitly[1] or explicitly drawing a sample from a uniform distribution over that Interval using the global PRNG instance.

bar.xform = ▨₅

Path(points= ▨₆)

Expressions that would be large blobs of code can be collapsed/elided/hidden, getting replaced with a symbol like

This can be helpful for things such as:

  • Motion capture data
  • Path points for pencil strokes

More details

You can think of this like importing a constant from an external module, except that:

"Where are the bytes?"

  • These expressions are all defined at the end of the same .artc code file (▨₅ = …)

Text editor extensions can optionally ignore that region of code (e.g. by automatically creating a Folding Range) and then provide UX for editing the expression at the reference site.

How format-on-save helps with non-ASCII

Normalized resultvia ASCIIor type it
5 m/s²5 m/s2
45°45degOpt + Shift + 8
x ± yInterval.plus_or_minus(x, y)Opt + Shift + +
random( ± 5cm )random(Interval.plus_or_minus(0, y))Opt + Shift + +

Standard library


Binary encoding



       .artc code    <------->      binary-encoded
        (as text)                 form of .artc code

Questions

What works of art are we capable of expressing in at most 100 bytes of [additional] information?

If there's an information-storage bottleneck someday, how much art could losslessly survive?

Design goals

Examples

Example 1

B26B2 is a 2.5-byte encoding of the following:



    scene.aspect_ratio = "√2:1"
    scene.background = Gray(25%)
    scene.padding = 6%

Note: this is a common initial pattern for concise 2D works ("set aspect ratio, background, and a padding percentage beyond the bounding box of whatever shapes end up in the scene graph"), so the encoding optimizes for it.

Full walk-through of this example

Background:
  • Current state: there's a finite set of states, each of which has its own Huffman tree which maps input-nibble prefix code sequences to action bundles. Some states (like .end_sequence_b2_init) map the empty string to one specific action bundle and do nothing else.
  • Input chunk: a full prefix code of one or more nibbles, which take us all the way from the root of this state's Huffman tree to one of its leaves
  • Action bundle: a named list of actions
  • State stack: like a to-do list, defining upcoming states. See also: Pushdown automaton
  • Node stack: a temporary stack of AST nodes, to be used soon in upcoming action bundles
  • Code: the AST that's been built so far. Its root is a .Module node.
Current stateInput chunkAction bundle for this (state, input)
.initial

Possible input

B.use_draft_b
B
(for "Draft B")
.use_draft_b:

  • Set state to .b_initial

Result

  • State stack: [.b_initial]
  • Node stack: []
.b_initial

Possible input

2 → 2D a;b;p
3 → 3D main
else : (reserved)
2
(for "2D with aspect / background / padding")
.start_sequence_b2_init:

  • Set state stack to [.end_sequence_b2_init]
  • Push state .get_aspect_ratio
  • Push state .get_background_color
  • Push state .get_padding_percent

Result

  • State stack: [.end_sequence_b2_init, .get_aspect_ratio, .get_background_color, .get_padding_percent]
  • Node stack: []
.get_padding_percent

Possible input

0 → 0
1 → 1%
2 → 2%
3 → 3%
4 → 4%
5 → 5%
6 → 6%
7 → 7%
8 → 8%
9 → 9%
A → 10%
B → 15%
C → 20%

D*, E*, and F* are reserved. At least one of them will lead to additional nibbles.
6.make_padding_percent with value=6:

  • Make temp node 6% (an AST node of type .Constant and subtype .ConstantQuantityPercent)

Result

  • State stack: [.end_sequence_b2_init, .get_aspect_ratio, .get_background_color]
  • Node stack: [6%]
.get_background_color

Possible input

0 → Gray(0)
1 → Gray(10%)
2 → Gray(20%)
3 → Gray(30%)
4 → Gray(40%)
5 → Gray(50%)
6 → Gray(60%)
7 → Gray(70%)
8 → Gray(80%)
9 → Gray(90%)
A → Gray(1)
B → Gray(25%)
C → Gray(75%)

D*, E*, and F* are reserved. At least one of them will lead to additional nibbles and the ability to express RGB or HSL colors.
B.make_gray_percent with value=25:

  • Make temp node Gray(25%) (node type .Call, containing a .Constant)

Result

  • State stack: [.end_sequence_b2_init, .get_aspect_ratio]
  • Node stack: [6%, Gray(25%)]
.get_aspect_ratio

Possible input

0 → "16:9"
1 → '"3:2"'
2 → '"√2:1"'
4 → '"4:3"'

5*, 6* : (reserved)
7 → "1:1"
8*, 9*, A*, B* : (reserved)

C → "3:4"
D → "1:√2"
E → "2:3"
F → "9:16"
2.make_string with value = (index of "√2:1" in the list of string constants):

  • Make temp node "√2:1" (a .Constant of subtype .ConstantStr)

Result

  • State stack: [.end_sequence_b2_init]
  • Node stack: [ 6%, Gray(25%), "√2:1" ]
.end_sequence_b2_init

Possible input

  • None. This state doesn't read any input.
.end_sequence_b2_init:

  • Append node scene.aspect_ratio = 🍿
    (an .Assign node, where 🍿 is whatever gets popped from the node stack)
  • Append node scene.background = 🍿
  • Append node scene.padding = 🍿
  • Set state to .b2_main

Result

  • State stack: [.b2_main]
  • Node stack: []
  • Code:
scene.aspect_ratio = "√2:1"
scene.background = Gray(25%)
scene.padding = 6%

Project status

Not yet published.

Project itinerary

  1. Not yet published
  2. Draft WASM blob (+ wrapper code) available at no cost
  3. FLOSS, v1.0