Modeling

Concepts

Child shapes

Zdog models are built with shapes. Shapes can be positioned with translate. Their positions are relative. For instance, when added to an Illustration, shapes are positioned relative to the Illustration’s origin.

let zCircle = new Zdog.Ellipse({
  addTo: illo,
  translate: { z: 40 }, // z +40 from illo
  // ...
});

let xRect = new Zdog.Rect({
  addTo: illo,
  translate: { x: 40 }, // x +40 from illo
  // ...
});

let yTri = new Zdog.Polygon({
  addTo: illo,
  translate: { y: -60 }, // y -60 from illo
  // ...
});

Shapes can be added as children to other shapes. A child shape is positioned relative to its parent.

let zCircle = new Zdog.Ellipse({
  addTo: illo,
  translate: { z: 40 }, // z +40 from illo
  // ...
});

let xRect = new Zdog.Rect({
  addTo: zCircle,
  translate: { x: 40 }, // x +40 from zCircle
  // ...
});

let yTri = new Zdog.Polygon({
  addTo: xRect,
  translate: { y: -60 }, // y -60 from xRect
  // ...
});

translate is a transform — as is rotate and scale. Child shapes inherit the transforms of their parents (wow, that is deep).

let zCircle = new Zdog.Ellipse({
  addTo: illo,
  scale: 1.5, // scale 150%
  translate: { z: 40 },
  rotate: { z: -Zdog.TAU/8 }, // rotate 45° CCW
  // ...
});

let xRect = new Zdog.Rect({
  addTo: zCircle,
  translate: { x: 40 },
  rotate: { x: Zdog.TAU/8 }, // rotate back
  // ...
});

let yTri = new Zdog.Polygon({
  addTo: xRect,
  translate: { y: -60 },
  // ...
});

Using child shapes and their additive transforms enables you to build complex models.

Anchors

An Anchor is an invisible shape. Use an Anchor for transforms without rendering a shape.

let zAnchor = new Zdog.Anchor({
  addTo: illo,
  scale: 1.5,
  translate: { z: 40 },
  rotate: { z: -Zdog.TAU/8 },
});

let xAnchor = new Zdog.Anchor({
  addTo: zAnchor,
  translate: { x: 40 },
  rotate: { x: Zdog.TAU/8 },
});

let yTri = new Zdog.Polygon({
  addTo: xAnchor,
  translate: { y: -60 },
  // ...
});

Vector Objects

Properties like translate, rotation, and scale are Vectors. A Vector can be set with a vector Object.

A vector Object is a plain ol' JavaScript Object with x, y, z coordinate properties. The coordinate properties are optional. They default to 0 if undefined. So you only need to set non-zero values.

translate: { x: 1, z: 2 }, // => { x: 1, y: 0, z: 2 }
translate: { y: 3 },       // => { x: 0, y: 3, z: 0 }
translate: {}              // => { x: 0, y: 0, z: 0 }

Copying

Copy items with .copy().

// create original
let rect = new Zdog.Rect({
  addTo: illo,
  width: 64,
  height: 64,
  translate: { x: -48 },
  stroke: 16,
  color: '#EA0',
});
// copy
rect.copy({
  // overwrite original options
  translate: { x: 48 },
  color: '#C25',
});

Copy items with their children with .copyGraph().

// create original
let rect = new Zdog.Rect({
  // ...
});
// add child item
new Zdog.Shape({
  addTo: rect,
  // ...
});
// copy rect and its children
rect.copyGraph({
  // overwrite original rect options
  translate: { x: 48 },
  color: '#C25',
});

Stroke volume

Whereas polygonal 3D engines rely on meshes of polygons to depict volume, Zdog shapes can show volume with stroke.

Look at this tasty burger. The patty and cheese slice are just simple circles. The sesame seeds are just lines. But with thick stroke they appear as plump round discs and pills.

// cheese
new Zdog.Rect({
  width: 92,
  height: 92,
  stroke: 16,
  // ...
});
// patty
new Zdog.Ellipse({
  diameter: 72,
  stroke: 28,
  // ...
});
// seed
new Zdog.Shape({
  path: [ { y: -3 }, { y: 3 } ],
  stroke: 8,
  // ...
});

Using stroke for volume is what makes Zdog special. Let go of your earthly polygons and become one with the round thickness.

Groups

Use a Group to control rendering order. Shapes will be rendered in the order they are added to the Group. Groups are useful for positioning shapes within other shapes, like windows in walls or pupils in eyes.

// render shapes in order added
var eyeGroup = new Zdog.Group({
  addTo: illo,
  translate: { z: 20 },
});
// eye white first
new Zdog.Ellipse({
  addTo: eyeGroup,
  width: 160,
  height: 80,
  // ...
});
// then iris
let iris = new Zdog.Ellipse({
  addTo: eyeGroup,
  diameter: 70,
  // ...
});
// then pupil
iris.copy({
  diameter: 30,
  color: '#636',
});
// highlight last in front
iris.copy({
  diameter: 30,
  translate: { x: 15, y: -15 },
  color: 'white',
});

Modeling tutorial

Modeling with Zdog is done by positioning and combining shapes to make more complex objects. This tutorial will walk through modeling this high-struttin' dude.

Head & face

Our initial setup picks up from the Getting started demo. We have an Illustration, a model with a single Shape, and an animation loop.

let illo = new Zdog.Illustration({
  element: elem,
  zoom: 10,
  dragRotate: true,
});

// ---- model ---- //

let head = new Zdog.Shape({
  addTo: illo,
  stroke: 12,
  color: gold,
});

// -- animate --- //

function animate() {
  illo.updateRenderGraph();
  requestAnimationFrame( animate );
}
animate();

The head shape is rendered as a flat-colored sphere with Shape. The Shape class can be defined to render any shape — lines, curves, polygons — via its path property. As head does not have path set, its path defaults to a single point. That point, with stroke volume, renders a flat-colored sphere. In other words, a circle.

Next we add the eye Ellipse as a child shape to head with addTo: head.

let eye = new Zdog.Ellipse({
  addTo: head,
  diameter: 2,
  quarters: 2, // semi-circle
  translate: { x: -2, y: 1, z: 4.5 },
  // rotate semi-circle to point up
  rotate: { z: -TAU/4 },
  color: eggplant,
  stroke: 0.5,
  // hide when front-side is facing back
  backface: false,
});

For the eye on the right, we can .copy() the right. The original options are copied over and then can be overwritten with new options, in this case changing translate.

// eye on left
let eye = new Zdog.Ellipse({
  addTo: head,
  diameter: 2,
  quarters: 2,
  translate: { x: -2, y: 1, z: 4.5 },
  // ...
});
// eye on right
eye.copy({
  translate: { x: 2, y: 1, z: 4.5 },
});

Compare the translate vector Objects for the eyes.

// eye on left
translate: { x: -2, y: 1, z: 4.5 }
// eye on right
translate: { x: 2, y: 1, z: 4.5 }

The only difference is the x coordinate. But all three x, y, z coordinates need to be set. Setting just translate: { x: 2 } would yield { x: 2, y: 0, z: 0 } which is not what we want.

The smile is made with a similar semi-circle Ellipse. Its path is closed with closed: true.

// smile
new Zdog.Ellipse({
  addTo: head,
  diameter: 3,
  quarters: 2,
  translate: { y: 2.5, z: 4.5 },
  rotate: { z: TAU/4 },
  closed: true,
  color: '#FED',
  stroke: 0.5,
  fill: true,
  backface: false,
});

Body core

Let’s give this floating head a body starting with the hips.

// illo zoom: 5
// remove head for now

let hips = new Zdog.Shape({
  addTo: illo,
  path: [ { x: -3 }, { x: 3 } ],
  stroke: 4,
  color: '#636',
});

hips is just a horizontal line Shape. Unlike head, hips has its path set. The path is set to an Array with two vector Objects. So this path reads: start at x: -3, draw a line to x: 3.

We start with the hips because the model’s upper body pivots around the hips. By adding the upper body shapes to hips, we can rotate the hips and shapes will rotate with it.

Let’s work our way up and add the chest next.

let chest = new Zdog.Shape({
  addTo: hips,
  path: [ { x: -1.5 }, { x: 1.5 } ],
  // position right above hips
  // ( hips.stroke + chest.stroke ) / 2
  translate: { y: -6.5 },
  stroke: 9,
  color: '#C25',
});

chest is another horizontal line Shape. Ample stroke gives makes this little line a big barrelled torso.

The head can now go back on top.

let head = new Zdog.Shape({
  addTo: chest,
  stroke: 12,
  // position above chest
  translate: { y: -9.5 },
  color: '#EA0',
});

// other face shapes...

Now we can rotate illo and hips to see a preview of the final product.

let illo = new Zdog.Illustration({
  zoom: 5,
  // rotate for three-quarters view
  rotate: { y: -Zdog.TAU/8 },
  // ...
});

let hips = new Zdog.Shape({
  addTo: illo,
  // tilt back 45°
  rotate: { x: Zdog.TAU/8 },
  // ...
});

So chill.

Legs

Let’s get back upright and now work on the lower half.

let hipX = 3;

let hips = new Zdog.Shape({
  path: [ { x: -hipX }, { x: hipX } ],
  // ...
});

let leg = new Zdog.Shape({
  addTo: hips,
  path: [ { y: 0 }, { y: 12 } ],
  translate: { x: -hipX },
  color: '#636',
  stroke: 4,
});

We can render a leg with a straight line Shape. This line will start at the left hip. So we add the leg to hips and set its origin with translate: { x: -hipX }. We can use a variable hipX within both hips.path and leg.translate for consistency. Now we can draw the path starting at the origin 0 and go down, hence path: [ { y: 0 }, { y: 12 } ].

Next, the foot.

// foot
new Zdog.RoundedRect({
  addTo: leg,
  width: 2,
  height: 4,
  cornerRadius: 1,
  // y: past leg end, z: scootch toward front
  translate: { y: 14, z: 2 },
  color: '#C25',
  fill: true,
  stroke: 4,
});

The foot is first shape we need to consider in three dimensions. The toe sticks out toward the front. We could draw a custom Shape that works with z values. But instead, a simpler approach is to rotate a basic shape into the desired orientation.

This simple foot is rendered with a RoundedRect. The corners are completely rounded with cornerRadius: 1, forming a flattened pill shape. The foot is added to leg and positioned vertically a little past the end of the leg, and along z toward the front so the toe sticks out farther than the heel.

Now let’s rotate that foot.

// foot
new Zdog.RoundedRect({
  addTo: leg,
  translate: { y: 14, z: 2 },
  // rotate 90° along x-axis
  rotate: { x: Zdog.TAU/4 }
  // ...
});

Perfecto. Now that one leg is in place, we can copy the leg and its foot with .copyGraph().

leg.copyGraph({
  // position on right
  translate: { x: hipX },
});

Arms

This fella needs some dukes for puttin' up. We'll use the same technique with the legs, this time working off the chest.

var armSize = 6;

// left arm
let upperArm = new Zdog.Shape({
  addTo: chest,
  path: [ { y: 0 }, { y: armSize } ],
  translate: { x: -5, y: -2 },
  color: '#636',
  stroke: 4,
});

Whereas the leg was just one straight line from hip to foot, the arm will require two parts. The first upperArm shape starts from the chest.

let forearm = new Zdog.Shape({
  addTo: upperArm,
  path: [ { y: 0 }, { y: armSize } ],
  translate: { y: armSize },
  color: '#EA0',
  stroke: 4,
});

The forearm connects to the upperArm. It starts at the end of the upperArm by setting translate: { y: armSize }, which matches upperArm’s end path point.

// hand
new Zdog.Shape({
  addTo: forearm,
  // connect to end of forearm
  // scootch toward front a bit
  translate: { y: armSize, z: 1 },
  stroke: 6,
  color: '#EA0',
});

The hand is rendered as a single-point Shape like the head. It connects to forearm.

// copy to right arm
upperArm.copyGraph({
  translate: { x: 5, y: -2 },
});

Whoa! All the body parts are in place.

Rotating the model around, you'll notice how the shapes pop-over one another — like where the upperArm and forearm overlap on the elbow. This is called z-fighting. We'll clean this up a bit as we rotate shapes away from one another. But these z-fights are innevitable with pseudo-3D engine like Zdog. Rather than fighting this effect, accept it for what it is. It’s not a bug, it’s what makes Zdog charming.

Rotating legs

Let’s kick it. First let’s adjust the position and rotation of the figure for better viewing.

const TAU = Zdog.TAU; // easier to read constant

let illo = new Zdog.Illustration({
  // rotate for three-quarters view
  rotate: { y: -TAU/8 },
  // ...
});

let hips = new Zdog.Shape({
  // move down a little
  translate: { y: 2 },
  // ...
});

The leg on the left needs to be rotated 90°. As a kick would be rotating the leg’s horizontal axis, that means rotating around the x axis.

let leg = new Zdog.Shape({
  addTo: hips,
  path: [ { y: 0 }, { y: 12 } ],
  translate: { x: -hipX },
  // rotate leg
  rotate: { x: TAU/4 },
  color: eggplant,
  stroke: 4,
});

I like to use TAU in to set rotate values. TAU is a full rotation in radians, equal to 2 * Math.PI. So TAU/4 is a quarter-turn.

Items rotate around their origin. The leg’s origin is its first point, which we set in its path, [ { y: 0 }, { y: 12 } ].

Both legs are rotated because the leg on the right copied the properties of the left. So let’s rotate the right leg to its position.

// leg on left
let leg = new Zdog.Shape({
  translate: { x: -hipX },
  rotate: { x: TAU/4 },
  // ...
});
// foot
new Zdog.RoundedRect({
  addTo: leg,
  // ...
});
// leg on right
leg.copyGraph({
  translate: { x: hipX },
  rotate: { x: -TAU/8 },
});

The legs are in the place. But the bottom foot needs to look flat on the ground. Again, it’s copied over its rotate value from the other foot. So we need to overwrite it.

// kick foot
let foot = new Zdog.RoundedRect({
  addTo: leg,
  rotate: { x: TAU/4 },
  // ...
});
// leg on right
let standLeg = leg.copy({
  translate: { x: hipX },
  rotate: { x: -TAU/8 },
});
// stand foot
foot.copy({
  addTo: standLeg,
  rotate: { x: -TAU/8 },
});

To separate the feet, we change leg.copyGraph() to leg.copy(). Now the kick foot can be copied, added to standLeg, and have its rotate value set.

And a perfectly normal-looking gait is rendered. So realistic!

Rotating spine

Let’s tilt the figure back upper body back by rotating the hips.

let hips = new Zdog.Shape({
  rotate: { x: TAU/8 },
  // ...
});

Uh oh. By rotating hips the entire figure was rotated — as all the shapes are descendents of hips.

One way to resolve this is by adding a separate Anchor to rotate just the upper body shapes.

// separate anchor just for rotating upper body
let spine = new Zdog.Anchor({
  addTo: hips,
  rotate: { x: TAU/8 },
});

let chest = new Zdog.Shape({
  // add chest to spine instead of hips
  addTo: spine,
  // ...
});

Last step: arms.

Rotating arms

The arms can be rotated just like the legs.

// arm on left
let upperArm = new Zdog.Shape({
  translate: { x: -5, y: -2 },
  // rotate back 90°
  rotate: { x: -TAU/4 },
  // ...
});

// forearm & hand...

// arm on right
upperArm.copyGraph({
  translate: { x: 5, y: -2 },
  // rotate forward 90°
  rotate: { x: TAU/4 },
});

Both forearm and hand shapes continue to stick straight away from the upperArm. To bend the elbow, we can rotate forearm.

let forearm = new Zdog.Shape({
  addTo: upperArm,
  rotate: { x: TAU/8 },
  // ...
});

We only have to rotate the one forearm as the relative angle of rotation of the forearms compared to their parent upper-arm shapes is the same.

It’s done!

Now that the model is complete, try going back and make some changes. How would you rotate the arms for a T-pose? How could you break up the legs into thigh and shin like we did for the arms? What shapes could be used to show an angry face?

This tutorial covers the basic of shape composition with Zdog. Now you're ready to start making your own models. Take a look at API for all of Zdog’s functionality and Shapes for all the shapes.