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

Ideal Calder DSL Language Discussion, and pain points with current programs #98

Open
armcburney opened this issue Jun 15, 2018 · 7 comments
Assignees

Comments

@armcburney
Copy link
Member

armcburney commented Jun 15, 2018

As per our discussion in the hangouts chat, here's a place we can discuss ideas for a Calder DSL, as well as pain points with the way we currently use the library.

Current Calder Code

///////////////////////////////////////////////////////////////////////////////////////////////////
// Step 1: setup lights
///////////////////////////////////////////////////////////////////////////////////////////////////

// Create light sources for the renderer
const light1 = new Light({
    position: { x: 10, y: 10, z: 10 },
    color: RGBColor.fromHex('#FFFFFF'),
    intensity: 256
});
const light2 = new Light({
    position: { x: 700, y: 500, z: 50 },
    color: RGBColor.fromHex('#FFFFFF'),
    intensity: 100
});

// Add lights to the renderer
renderer.addLight(light1);
renderer.addLight(light2);

///////////////////////////////////////////////////////////////////////////////////////////////////
// Step 2: create geometry
///////////////////////////////////////////////////////////////////////////////////////////////////

// Setup leaf
const leafColor = RGBColor.fromRGB(204, 255, 204);
const workingLeafSphere = Shape.sphere(leafColor);
const leafSphere = workingLeafSphere.bake();

// Setup branch
const branchColor = RGBColor.fromRGB(102, 76.5, 76.5);
const workingBranchShape = Shape.cylinder(branchColor);
const branchShape = workingBranchShape.bake();

///////////////////////////////////////////////////////////////////////////////////////////////////
// Step 3: create armature
///////////////////////////////////////////////////////////////////////////////////////////////////

const bone = Armature.define((root) => {
    root.createPoint('base', { x: 0, y: 0, z: 0 });
    root.createPoint('mid', { x: 0, y: 0.5, z: 0 });
    root.createPoint('tip', { x: 0, y: 1, z: 0 });
});

const treeGen = Armature.generator();
const tree = treeGen
    .define('branch', 1, (root) => {
        const node = bone();
        node.point('base').stickTo(root);
        const theta = Math.random() * 45;
        const phi = Math.random() * 360;
        node.setRotation(Matrix.fromQuat4(Quaternion.fromEuler(theta, phi, 0)));
        node.setScale(Matrix.fromScaling({ x: 0.8, y: 0.8, z: 0.8 })); // Shrink a bit

        const trunk = node.point('mid').attach(branchShape);
        trunk.setScale(Matrix.fromScaling({ x: 0.2, y: 1, z: 0.2 }));

        // branching factor of 2
        treeGen.addDetail({ component: 'branchOrLeaf', at: node.point('tip') });
        treeGen.addDetail({ component: 'branchOrLeaf', at: node.point('tip') });
    })
    .define('branchOrLeaf', 1, (root) => {
        treeGen.addDetail({ component: 'leaf', at: root });
    })
    .define('branchOrLeaf', 4, (root) => {
        treeGen.addDetail({ component: 'branch', at: root });
    })
    .define('leaf', 1, (root) => {
        const leaf = root.attach(leafSphere);
        const scale = Math.random() * 0.5 + 0.5;
        leaf.setScale(Matrix.fromScaling({ x: scale, y: scale, z: scale }));
    })
    .generate({ start: 'branch', depth: 15 });

///////////////////////////////////////////////////////////////////////////////////////////////////
// Step 4: set up renderer
///////////////////////////////////////////////////////////////////////////////////////////////////

document.body.appendChild(renderer.stage);

renderer.camera.moveTo({ x: 0, y: 0, z: 8 });
renderer.camera.lookAt({ x: 2, y: 2, z: -4 });

// Draw the armature
const draw = () => {
    return {
        objects: [tree],
        debugParams: { drawAxes: true, drawArmatureBones: false }
    };
};

// Apply the constraints each frame.
renderer.eachFrame(draw);
@davepagurek
Copy link
Member

Also we have #90 to deal with the matrix and quaternion things currently in the example code, so we can pretend those won't be there when thinking about syntax

@armcburney
Copy link
Member Author

armcburney commented Jun 15, 2018

I'll start with my current pain points which I think could be improved. Please keep in mind this isn't an attack on the way anything was implemented, for example we didn't have the coord type until very recently, so a lot of the legacy stuff couldn't be written any other way than specifying a 3-length array for instance.

Current pain points

1. Light usage ✅

Closed in #104.

Current usage

/**
 * I think having `Light` class would be more explicit about what we're creating rather
 * than a generic `JSON` object which implicitly requires these three fields
 */
const light1 = {
    lightPosition: [10, 10, 10], // might be nice to use the `coord` type instead for consistency
    lightColor: [0.3, 0.3, 0.3], // would be nice to use a `Color` instead
    lightIntensity: 256
};

Ideal "JavaScript ecosystem" usage

const light1 = new Light({
    lightPosition: { x: 10, y: 10, z: 10 },
    lightColor: RGBColor.fromHex("#1E1E1E"),
    lightIntensity: 256
});

2. L-systems

I like this pattern a lot as a programmer, but as an artist (with no prior programming knowledge) it might be a little confusing with the lambdas (nested code inside it, syntax), and chaining.

@armcburney
Copy link
Member Author

armcburney commented Jun 18, 2018

Ideal Calder DSL Proposition

Overview

This is an early proposition for my ideal Calder DSL for artists and animators. Much of this is subject to change, and I would encourage and welcome constructive feedback.

The syntax is influenced by the languages Ruby, MoonScript, Haxe, and Qt QML.

Reference

Variable Assignment

Assigning to an undeclared name will cause it to be declared as a new local variable. The language is dynamically typed so you can assign any value to any variable. You can assign multiple names and values at once like in Lua or MoonScript.

import rad, deg from Angle

angle = rad(0.5)
theta, phi, zeta = deg(30, 120, 90)

Update Assignment

+=, -=, /=, *=, %=, or=, and= operators have been added for updating and assigning at the same time. They are aliases for their expanded equivalents.

a = 0
a += 10

Comments

Comments in Calder are prefixed with --. This was borrowed from MoonScript/Lua, since - is usally a character used for bulleted lists, which I think is a good analogy to a comment.

-- this is a comment!

-------------------------
-- this is another one --
-------------------------

String Interpolation

You can mix expressions into string literals like in Ruby using the #{} syntax.

log "I am #{math.random! * 100}% sure."

For Loop

For loops use the similar each syntax for seen in Ruby.

list = [1, 2, 3]

list.each do |i|
  log i
end

Mapping

list = [1, 2, 3]
squared = list.map do |i|
  i * i
end

While Loops

until condition do
  log "do stuff"
end
loop do
  log "do stuff"
end
while condition do
  log "do stuff"
end

Conditionals

if/else
if cond
  log "cond"
else
  log "no cond"
end

A short syntax for single statements can also be used:

if cond log "cond" else log "no cond"

Because if statements can be used as expressions, this can also be written as:

log if cond "cond" else "no cond"
switch
switch value
  when "a"
    log "first condition"
  when "b"
    log "second condition"
  when "c"
    log "third condition"
end

For cases where only one line is required, this may be written as:

switch value do
  when "a" log "first condition"
  when "b" log "second condition"
  when "c" log "third condition"
end

Native JavaScript

Like in Haxe, we should have the ability for a user to write their own JavaScript if need be. This shouldn't be encouraged, but the option should be available for developers who require more advanced features.

javascript do
  // This is EE6 JavaScript code
  const angle = Math.random() * 0.5;
  console.log(angle);
end

-- This is calder code
log(angle)

...

Still a WIP, I'll update this comment with the rest of the spec in my head.

Tree Example

---------------------------------
-- Step 1: Setup lights
---------------------------------

-- Create first light source
light1 = Light(
  position: Point(x: 10, y: 10, z: 10),
  color: Color(hex: #FFFFFF),
  intensity: 256
)

-- Create second light source
light2 = Light(
  position: Point(x: 700, y: 500, z: 50),
  color: Color(hex: #FFFFFF),
  intensity: 100
)

-- Add lights to the renderer
renderer.add_light(light1)
renderer.add_light(light2)

---------------------------------
-- Step 2: Create geometry
---------------------------------

-- Setup leaf
leaf_sphere = Sphere(
  color: Color(red: 204, green: 255, blue: 204),
  bake: true
)

-- Setup branch
branch_shape = Cylinder(
  color: Color(red: 102, green: 76.5, blue: 76.5),
  bake: true
)

---------------------------------
-- Step 3: Create armature
---------------------------------

bone = Armature.define do |root|
  root.create_point(name: base, location: Point(x: 0, y: 0, z: 0))
  root.create_point(name: mid, location: Point(x: 0, y: 0.5, z: 0))
  root.create_point(name: tip, location: Point(x: 0, y: 1, z: 0))
end

tree = Armature.generator do |tree_generator|
  tree_generator.define name: branch, weight: 1, spawn_point: do
    node = bone
    node_base = node.point(name: base)
    node_base.stick_to(spawn_point)

    theta = Random.integer * 45
    phi = Random.integer * 360

    node.rotate(theta: theta, phi: phi, zeta: 0)
    node.scale(x: 0.8, y: 0.8, z: 0.8)

    middle_node = node.point(name: mid)
    trunk = middle_node.attach(branch_shape)
    trunk.scale(x: 0.2, y: 0.1, z: 0.2)

    -- branching factor of 2
    tree_generator.add_detail(component: branch_or_leaf, at: node.point(name: tip))
    tree_generator.add_detail(component: branch_or_leaf, at: node.point(name: tip))
  end

  tree_generator.define name: branch_or_leaf, weight: 1, spawn_point: do
    tree_generator.add_detail(component: leaf, at: spawn_point)
  end

  tree_generator.define name: branch_or_leaf, weight: 4, spawn_point: do
    tree_generator.add_detail(component: branch, at: root)
  end

  tree_generator.define name: leaf, weight: 1, spawn_point: do
    leaf = spawn_point.attach(leaf_sphere)
    scale = Random.integer * 0.5 + 0.5
    leaf.scale(x: scale, y: scale, z: scale)
  end

  tree_generator.generate(start: branch, depth: 15)
end

---------------------------------
-- Step 4: Setup renderer
---------------------------------

-- Move the camera to the desired location
renderer.camera.move_to(Point(x: 0, y: 0, z: 8))
renderer.camera.look_at(Point(x: 2, y: 2, z: -4))

-- Apply the constraints each frame
angle = 0

-- Perform this operation each frame
renderer.each_frame draw: do
  angle += 0.5
  tree.rotate(theta: 0, phi: angle, zeta: 0)
  return objects: [tree], draw_axes: true, draw_armature_bones: false
end

@davepagurek
Copy link
Member

This is a little out there, but what do you think of the idea of not using general lambdas for setup? When we're using them as setup blocks, we know the lambda is being used for a specific purpose (given an input, initialize it), so we don't need total freedom for parameters and return values.

My proposal is to add a lambda syntax that reuses the key as the lambda input. Going even further, we can "overload" based on the provided keys so that we don't have redundant terms (like Armature.defineComponent(component: { ... }).

So it might look like this:

bone = Armature.define component: {
  component.create_point(name: base, location: Point { x: 0, y: 0, z: 0 })
  component.create_point(name: mid, location: Point { x: 0, y: 0.5, z: 0 })
  component.create_point(name: tip, location: Point { x: 0, y: 1, z: 0 })
}

tree = Armature.define generator: {
  generator.define(name: branch, spawn_point: {
    node = bone
    node_base = node.point(name: base)
    node_base.stick_to(spawn_point)

    theta = Random.integer * 45
    phi = Random.integer * 360

    node.rotate(theta: theta, phi: phi, zeta: 0)
    node.scale(x: 0.8, y: 0.8, z: 0.8)

    middle_node = node.point(name: mid)
    trunk = middle_node.attach(branch_shape)
    trunk.scale(x: 0.2, y: 0.1, z: 0.2)

    -- branching factor of 2
    generator.add_detail(component: branch_or_leaf, at: node.point(name: tip))
    generator.add_detail(component: branch_or_leaf, at: node.point(name: tip))
  })

  tree_generator.define(name: branch_or_leaf, spawn_point: {
    tree_generator.add_detail(component: leaf, at: spawn_point)
  })

  -- ...

}

tree = tree_generator.generate(start: branch, depth: 15)

This isn't perfect (the spawn_point thing in particular is still weird), but overall my goal is to reduce the complexity of what users need to think about, and thinking about what a lambda is might be a lot.

@abhimadan
Copy link
Member

As we talked about online, I think we should try to optimize for readability in this code. In large programs we see at work that modularize logic into functions, classes, and so on, we would take a DFS approach to reading code. However, I think this is a daunting prospect for new programmers, since this usually requires multiple passes to understand what the code is doing. I propose we design the syntax with the following assumptions:

  1. There are no external libraries.
  2. The code can be read in a single pass from top to bottom (like a simple Python script, for example).
  3. Tend towards verbosity (but not excessively so) so that the code can be read by people not familiar with the language.

We can relax some of those assumptions later (for example, we might want users to share code so we may want a simple package management system), but I think we can avoid thinking about those complexities at the start, since it should be possible to extend the language to support these (for example, we can support modularity and top-to-bottom readability by enforcing declare-before-use).

@abhimadan
Copy link
Member

For example, something like this:

do
  -- something
end

For verbosity. We don't really see the end keyword like this in too many languages, but I think it's a nice addition to a beginner-friendly language since we're not using esoteric symbols like braces, and even if we think they're easy enough to understand, it might be a turn-off for people less inclined towards programming, who may feel they need to learn a lot to understand the language.

@davepagurek
Copy link
Member

@abhimadan I think since braces are a thing in CSS, it's maybe already familiar to the sorts of people who we're aiming this at (tech savvy but not programmers).

do..end makes sense if you think of it as defining a unit of computation like a function, but that's an abstract concept that we maybe don't want users to have to think about. e.g. if we only use them for setting things up, setup..end may be better? Basically I'm not opposed to using words instead of braces but I think we need to pick the words carefully and have them make sense in context

@pbardea pbardea removed their assignment Aug 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants