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.
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 },
// ...
});
Properties like translate
, rotation
, and scale
are Vector
s. 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 }
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',
});
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.
Use a Group
to control rendering order. Shapes will be rendered in the order they are added to the Group
. Group
s 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 with Zdog is done by positioning and combining shapes to make more complex objects. This tutorial will walk through modeling this high-struttin' dude.
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,
});
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.
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 },
});
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.
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!
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.
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.