Z-fighting is when two shapes pop-over one another as they appear to fight for position. In polygonal 3D engines, z-fighting happens when two polygons are coplanar. In Zdog, z-fighting happens when any shapes occupy the same space.
Z-fighting in Zdog is the natural result of its pseudo-3D engine. Zdog’s 3D shapes are actually 2D projections, rendered without any accounting for collisions with other shapes.
Rather than fighting against this effect, the best course is to accept it. Z-fighting is one of Zdog’s charms. Embrace it.
That said, there are some techniques to address z-fighting.
A simple solution is to use the same color for multiple shapes.
For shapes of similar size, move the shapes away from one another so their stroke volume do not overlap.
Use a Group
for groups of shapes that collectively cover the same area as a larger shape. In this demo, the gold dots are in a Group
, whereas the purple dots are individual shapes.
Use a Shape
with visible: false
in a Group
to balance out its z-index. Shape
z-index is calculated as the average of all the shape’s path points. Group
z-index is calcuated as the average of all the grouped shapes' z-indexes. So you can affect the z-index of a Group
by adding or moving invisible shapes.
In the demo below, the gold dot has a counter-balanced invisible shape in its group.
let group = new Zdog.Group({...});
// dot
new Zdog.Shape({
addTo: group,
stroke: 20,
translate: { x: 45, y: -45 },
color: '#EA0',
});
// invisible Shape to counter-balance group z-index
new Zdog.Shape({
addTo: group,
visible: false,
translate: { x: -45, y: 45 },
});
Due to Zdog’s underlying math, Hemisphere
, Cylinder
, and Cone
shapes cannot be scaled with a 1- or 2-dimensional Vector
.
let anchor = new Zdog.Anchor({
// 1-dimensional scale
// will break Hemisphere, Cone, & Cylinder
scale: { y: 2 },
});
new Zdog.Hemisphere({ addTo: anchor, /*...*/ });
new Zdog.Cylinder({ addTo: anchor, /*...*/ });
new Zdog.Cone({ addTo: anchor, /*...*/ });
However, uniform Vector
options will still work.
let anchor = new Zdog.Anchor({
// uniform scale, will be okay
scale: 2,
});
new Zdog.Hemisphere({ addTo: anchor, /*...*/ });
new Zdog.Cylinder({ addTo: anchor, /*...*/ });
new Zdog.Cone({ addTo: anchor, /*...*/ });
Gaps can appear in between flat polygons of solids.
new Zdog.Box({
addTo: illo,
width: 120,
height: 100,
depth: 80,
rotate: { x: -Zdog.TAU/8, y: Zdog.TAU/8 },
stroke: false,
color: '#EA0',
rearFace: '#636',
leftFace: '#636',
bottomFace: '#636',
});
You can cover over this gaps by adding a 1-pixel-wide stroke to the polygons.
// with an Illustration without zoom set
new Zdog.Box({
// 1px stroke to cover edge-to-edge gaps
stroke: 1,
// ...
});
// with an Illustration with zoom set
let illo = new Illustration({
zoom: 4,
// ...
});
new Zdog.Box({
addTo: illo,
stroke: 1 / illo.zoom, // 1px stroke
// ...
});
You can render Zdog models without using Illustration
, in the case you are directly using a <canvas>
or <svg>
and prefer to handle managing the element yourself.
The demos below work by using a generic Anchor
as the scene
object. The scene
’s graph is then updated with updateGraph
and rotated by dragging with a Dragger
.
Render the scene
’s graph on a <canvas>
with renderGraphCanvas
. Additional code is required to prepare the canvas for rendering. View this demo on CodePen.
// ----- setup ----- //
// get canvas element and its context
let canvas = document.querySelector('.zdog-canvas');
let ctx = canvas.getContext('2d');
// get canvas size
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
// illustration variables
const TAU = Zdog.TAU;
const zoom = 4;
let isSpinning = true;
// create an scene Anchor to hold all items
let scene = new Zdog.Anchor();
// ----- model ----- //
// add shapes to scene
new Zdog.Shape({
addTo: scene,
//...
});
new Zdog.Ellipse({
addTo: scene,
//...
});
// ----- animate ----- //
function animate() {
// make changes to model, like rotating scene
scene.rotate.y += isSpinning ? 0.03 : 0;
scene.updateGraph();
render();
requestAnimationFrame( animate );
}
function render() {
// clear canvas
ctx.clearRect( 0, 0, canvasWidth, canvasHeight );
ctx.save();
// center canvas & zoom
ctx.translate( canvasWidth/2, canvasHeight/2 );
ctx.scale( zoom, zoom );
// set lineJoin and lineCap to round
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
// render scene graph
scene.renderGraphCanvas( ctx );
ctx.restore();
}
animate();
// ----- drag ----- //
let dragStartRX, dragStartRY;
let minSize = Math.min( canvasWidth, canvasHeight );
// add drag-rotatation with Dragger
new Zdog.Dragger({
startElement: canvas,
onDragStart: function() {
isSpinning = false;
dragStartRX = scene.rotate.x;
dragStartRY = scene.rotate.y;
},
onDragMove: function( pointer, moveX, moveY ) {
scene.rotate.x = dragStartRX - ( moveY / minSize * TAU );
scene.rotate.y = dragStartRY - ( moveX / minSize * TAU );
},
});
Render the scene’s graph on a <svg>
with renderGraphSvg
. Additional code is required to prepare the SVG for rendering. View this demo on CodePen.
// ----- setup ----- //
// get svg element
let svg = document.querySelector('svg');
// rendering sizes
const zoom = 4;
let sceneWidth = 16;
let sceneHeight = 16;
let viewWidth = sceneWidth * zoom;
let viewHeight = sceneHeight * zoom;
let svgWidth = svg.getAttribute('width');
let svgHeight = svg.getAttribute('height');
// set viewBox using zoom
svg.setAttribute( 'viewBox', `${-viewWidth/2} ${-viewHeight/2} ` +
`${viewWidth} ${viewHeight}` );
// rendering variable
const TAU = Zdog.TAU;
let isSpinning = true;
// create an scene Anchor to hold all items
let scene = new Zdog.Anchor();
// ----- model ----- //
// add shapes to scene
new Zdog.Shape({
addTo: scene,
//...
});
new Zdog.Ellipse({
addTo: scene,
//...
});
// ----- animate ----- //
function animate() {
scene.rotate.y += isSpinning ? 0.03 : 0;
scene.updateGraph();
render();
requestAnimationFrame( animate );
}
function render() {
empty( svg );
scene.renderGraphSvg( svg );
}
animate();
function empty( element ) {
while ( element.firstChild ) {
element.removeChild( element.firstChild );
}
}
// ----- drag ----- //
// click drag to rotate
let dragStartRX, dragStartRY;
let minSize = Math.min( svgWidth, svgHeight );
// add drag-rotatation with Dragger
new Zdog.Dragger({
startElement: svg,
onDragStart: function() {
isSpinning = false;
dragStartRX = scene.rotate.x;
dragStartRY = scene.rotate.y;
},
onDragMove: function( pointer, moveX, moveY ) {
scene.rotate.x = dragStartRX - ( moveY / minSize * TAU );
scene.rotate.y = dragStartRY - ( moveX / minSize * TAU );
},
});
Should you use <canvas>
or <svg>
? Visually, canvas and SVG output the same exact image with Zdog. In practice, they each have their benefits.
Canvas is better for lots of shapes and smaller image sizes. Canvas renders on a flat image, so it doesn't have to manage a DOM. But its rendered with pixels, so it can be slower with larger sizes — particularly with high-resolution screens.
SVG is better for fewer shapes and larger image sizes. SVG is a vector format, so its resolution independent, perfect for large image sizes. However, all shapes have to be rendered and sorted with individual DOM elements, which can negatively impact performance and get your fan spinning.
Help me select new features by adding your 👍 reaction to GitHub issues for features you would like to see added. Hotly requested features include:
Hemisphere
, Cylinder
, and Cone
wedge shapesOther people’s stuff:
My stuff: