Skip to main content

Canvas Coordinates

Understanding the canvas coordinate system is essential for positioning elements correctly in Klint.

Coordinate System Basics

With the default origin: 'corner', the canvas uses a coordinate system where:

  • (0, 0) is the top-left corner
  • X increases to the right
  • Y increases downward

With origin: 'center', (0, 0) is the center, while X still increases right and Y still increases down.

const draw = (K) => {
K.background('#f0f0f0');
K.fillColor('black');

// Top-left corner
K.circle(0, 0, 10);
K.text('(0, 0)', 20, 20);

// Top-right corner
K.circle(K.width, 0, 10);
K.text(`(${K.width}, 0)`, K.width - 80, 20);

// Bottom-left corner
K.circle(0, K.height, 10);
K.text(`(0, ${K.height})`, 20, K.height - 10);

// Bottom-right corner
K.circle(K.width, K.height, 10);
K.text(`(${K.width}, ${K.height})`, K.width - 100, K.height - 10);

// Center
K.circle(K.width/2, K.height/2, 10);
K.text('Center', K.width/2 - 25, K.height/2 - 15);
};

Logical pixels and DPR

K.width, K.height, drawing coordinates, pointer coordinates, offscreen dimensions, and pixel helper coordinates are all expressed in logical CSS pixels. Klint scales the backing store and base transform internally using K.dpr.

For example, a 400 × 300 logical canvas at DPR 2 has an 800 × 600 backing store, but you still draw its bottom-right corner at (400, 300). Do not multiply coordinates by devicePixelRatio yourself.

Use dpr: 'default' to follow the device (capped by maxDpr, which defaults to 3), or pass a positive number for a fixed DPR.

Common Positions

Canvas Boundaries

const draw = (K) => {
// Useful position references
const left = 0;
const right = K.width;
const top = 0;
const bottom = K.height;
const centerX = K.width / 2;
const centerY = K.height / 2;

// Draw cross at center
K.strokeColor('red');
K.line(centerX - 20, centerY, centerX + 20, centerY);
K.line(centerX, centerY - 20, centerX, centerY + 20);
};

Grid Positions

const draw = (K) => {
K.background('#fff');

// Create a 3x3 grid
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const x = (col + 0.5) * (K.width / 3);
const y = (row + 0.5) * (K.height / 3);

K.fillColor('#333');
K.circle(x, y, 20);

K.alignText('center', 'middle');
K.text(`${col},${row}`, x, y);
}
}
};

Shape Anchor Points

Different shapes use different anchor points:

Circles - Center Point

const draw = (K) => {
// Circle is drawn from its center
const x = 100;
const y = 100;
const radius = 50;

K.fillColor('lightblue');
K.circle(x, y, radius);

// Mark the center
K.fillColor('red');
K.circle(x, y, 3);
};

Rectangles - Configurable origin

const draw = (K) => {
// Rectangle uses the top-left corner by default.
// K.setRectOrigin('center') changes its anchor to the center.
const x = 100;
const y = 100;
const width = 100;
const height = 50;

K.fillColor('lightgreen');
K.rectangle(x, y, width, height);

// Mark the anchor point
K.fillColor('red');
K.circle(x, y, 3);
};

Text - Varies by Alignment

const draw = (K) => {
const x = K.width / 2;
const y = K.height / 2;

// Default: left, alphabetic
K.fillColor('black');
K.text('Default', x, y);

// Center, middle
K.alignText('center', 'middle');
K.text('Centered', x, y + 50);

// Mark anchor points
K.fillColor('red');
K.circle(x, y, 3);
K.circle(x, y + 50, 3);
};

Transformations

Transformations change the coordinate system:

Translation

const draw = (K) => {
K.background('#f0f0f0');

// Draw at origin
K.fillColor('blue');
K.rectangle(0, 0, 50, 50);

// Translate coordinate system
K.push();
K.translate(100, 100);

// Now (0,0) is at (100,100)
K.fillColor('red');
K.rectangle(0, 0, 50, 50);

K.pop();
};

Rotation

const draw = (K) => {
K.background('#f0f0f0');

// Rotate around origin (0,0)
K.push();
K.rotate(Math.PI / 4); // 45 degrees
K.fillColor('green');
K.rectangle(0, 0, 100, 50);
K.pop();

// Rotate around center
K.push();
K.translate(K.width/2, K.height/2);
K.rotate(Math.PI / 4);
K.fillColor('purple');
K.rectangle(-50, -25, 100, 50); // Centered rectangle
K.pop();
};

Useful Patterns

Converting Mouse to Canvas Coordinates

function CanvasSketch() {
const { context, KlintMouse } = useKlint();
const { mouse } = KlintMouse();

const draw = (K) => {
K.background('#fff');

// Mouse coordinates are already in canvas space
K.fillColor('black');
K.text(`Mouse: ${mouse.x}, ${mouse.y}`, 10, 20);

K.fillColor('red');
K.circle(mouse.x, mouse.y, 10);
};

return <Klint context={context} draw={draw} />;
}

Centering Objects

const draw = (K) => {
// Center a rectangle
const rectWidth = 200;
const rectHeight = 100;
const x = (K.width - rectWidth) / 2;
const y = (K.height - rectHeight) / 2;
K.rectangle(x, y, rectWidth, rectHeight);

// Center a circle (already centered by default)
K.circle(K.width/2, K.height/2, 50);

// Center text
K.alignText('center', 'middle');
K.text('Centered Text', K.width/2, K.height/2);
};

Responsive Positioning

const draw = (K) => {
// Position elements as percentage of canvas
const margin = 0.1; // 10% margin

// Draw in corners with margin
const cornerSize = 30;

// Top-left
K.circle(K.width * margin, K.height * margin, cornerSize);

// Top-right
K.circle(K.width * (1 - margin), K.height * margin, cornerSize);

// Bottom-left
K.circle(K.width * margin, K.height * (1 - margin), cornerSize);

// Bottom-right
K.circle(K.width * (1 - margin), K.height * (1 - margin), cornerSize);
};

Circular Positioning

const draw = (K) => {
K.background('#000');

const centerX = K.width / 2;
const centerY = K.height / 2;
const radius = 100;
const count = 12;

for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;

K.fillColor(`hsl(${i * 30}, 70%, 50%)`);
K.circle(x, y, 20);
}
};

Debugging Coordinates

const draw = (K) => {
K.background('#fff');

// Draw grid
K.strokeColor('#ddd');
K.strokeWidth(1);

// Vertical lines
for (let x = 0; x <= K.width; x += 50) {
K.line(x, 0, x, K.height);
K.fillColor('#999');
K.textSize(10);
K.text(x.toString(), x + 2, 12);
}

// Horizontal lines
for (let y = 0; y <= K.height; y += 50) {
K.line(0, y, K.width, y);
K.fillColor('#999');
K.textSize(10);
K.text(y.toString(), 2, y - 2);
}

// Your drawing code here
K.fillColor('red');
K.circle(150, 200, 30);
};

Common Pitfalls

  1. Manual DPR scaling - Coordinates are already logical pixels; do not multiply them by K.dpr.
  2. Y-axis direction - Y increases downward in both origin modes.
  3. Origin mode - With origin: 'center', canvas coordinates range roughly from -width / 2 to width / 2.
  4. Rectangle anchor - Rectangles use the corner unless changed with setRectOrigin().
  5. Rotation origin - Rotation happens around the current (0,0) unless you translate first.
  6. Text baseline - Text anchoring follows the current alignText() setting.

Next Steps