Back to list
dmccreary

geometry-microsim

by dmccreary

A learning graph for a high-school geometry course. This graph is being used to test our new learning-graph viewers and editors.

1🍴 0📅 Jan 23, 2026

SKILL.md


name: geometry-microsim description: Refactoring patterns and best practices for creating maintainable geometry MicroSims using p5.js. Provides guidelines for local coordinate systems, push/pop/translate/scale patterns, mathematical accuracy, and clean code structure.

Geometry MicroSim Refactoring Patterns

Overview

This skill provides best practices for creating and refactoring geometry MicroSims in p5.js. These patterns make code more maintainable, mathematically accurate, and easier to debug.

When to Use This Skill

Use this skill when:

  • Creating new geometry MicroSims with multiple shapes or drawing regions
  • Refactoring existing geometry code that has complex manual positioning
  • Debugging geometry visualizations with incorrect labels or positioning
  • Building MicroSims with shapes that need to be scaled, rotated, or transformed

File Naming Convention

ALWAYS use main.html as the HTML filename for all MicroSims. This ensures consistent iframe embedding across the project.

Standard Directory Structure

Each MicroSim should have this structure:

docs/sims/[sim-name]/
├── main.html           # REQUIRED: The HTML file (always named main.html)
├── [sim-name].js       # JavaScript file with p5.js code
├── index.md            # Documentation page
├── [sim-name].png      # Screenshot/preview image
└── metadata.json       # Optional: MicroSim metadata

Live Server Preview URL

After creating or modifying a MicroSim, use this URL pattern for Live Server preview:

http://127.0.0.1:5500/docs/sims/[sim-name]/main.html

Iframe Embedding

Always use main.html in iframe references:

<iframe src="main.html" width="100%" height="450px" scrolling="no"></iframe>

For external embedding:

<iframe src="https://dmccreary.github.io/geometry-course/sims/[sim-name]/main.html" width="100%" height="450px" scrolling="no"></iframe>

Core Principle: Local Coordinate Systems

Always draw geometric shapes in local coordinates centered at the origin, then use transformations to position them.

The Push/Pop/Translate/Scale Pattern

Instead of calculating global positions for every vertex, use this pattern:

function drawShape(centerX, centerY, scaleFactor) {
  push();
  translate(centerX, centerY);
  scale(scaleFactor);

  // Draw shape at origin in local coordinates
  // All coordinates are relative to (0, 0)
  triangle(verts.A.x, verts.A.y, verts.B.x, verts.B.y, verts.C.x, verts.C.y);

  // Draw labels, angle arcs, etc. in same local system
  drawAngleLabels(verts);
  drawVertexLabels(verts);

  pop();
}

Benefits of This Approach

  1. Separation of concerns: Geometry calculation is separate from positioning
  2. Easier debugging: Shapes work correctly at origin before positioning
  3. Reusability: Same shape function works at any position/scale
  4. Cleaner code: No scattered offset calculations throughout code
  5. Accurate labels: Labels stay associated with correct vertices through transforms

Scale Factor Compensation

When scaling, compensate for stroke weights and text sizes:

push();
translate(centerX, centerY);
scale(scaleFactor);

// Compensate stroke weight so it appears consistent
strokeWeight(2 / scaleFactor);

// Compensate text size
textSize(12 / scaleFactor);

// Draw geometry...
pop();

Geometry Calculation Best Practices

Separate Vertex Calculation from Rendering

Create a function that calculates vertices in normalized coordinates:

// Calculate triangle vertices from angles using law of sines
// Returns vertices in local coordinates centered at origin
function calculateTriangleVertices(angles, baseLength) {
  let angleA = radians(angles[0]);
  let angleB = radians(angles[1]);
  let angleC = radians(angles[2]);

  // Using law of sines: a/sin(A) = b/sin(B) = c/sin(C)
  let sideC = baseLength;
  let k = sideC / sin(angleC);
  let sideA = k * sin(angleA);
  let sideB = k * sin(angleB);

  // Place vertices, then center at origin
  let Cx = 0, Cy = 0;
  let Bx = sideA, By = 0;
  let Ax = sideB * cos(angleC);
  let Ay = -sideB * sin(angleC);

  // Center the triangle at origin
  let centX = (Ax + Bx + Cx) / 3;
  let centY = (Ay + By + Cy) / 3;

  return {
    A: { x: Ax - centX, y: Ay - centY },
    B: { x: Bx - centX, y: By - centY },
    C: { x: Cx - centX, y: Cy - centY }
  };
}

Calculate Bounding Box for Auto-Scaling

function drawShapePanel(centerX, centerY, panelWidth, panelHeight) {
  let verts = calculateVertices();

  // Calculate bounding box
  let minX = min(verts.A.x, verts.B.x, verts.C.x);
  let maxX = max(verts.A.x, verts.B.x, verts.C.x);
  let minY = min(verts.A.y, verts.B.y, verts.C.y);
  let maxY = max(verts.A.y, verts.B.y, verts.C.y);
  let shapeWidth = maxX - minX;
  let shapeHeight = maxY - minY;

  // Scale to fit panel with padding
  let scaleX = (panelWidth - 40) / shapeWidth;
  let scaleY = (panelHeight - 40) / shapeHeight;
  let scaleFactor = min(scaleX, scaleY) * 0.85;

  push();
  translate(centerX, centerY);
  scale(scaleFactor);
  // Draw...
  pop();
}

Label Positioning

Vertex Labels

Position labels radially outward from centroid:

function drawVertexLabels(verts, scaleFactor) {
  let centX = (verts.A.x + verts.B.x + verts.C.x) / 3;
  let centY = (verts.A.y + verts.B.y + verts.C.y) / 3;

  let labelDist = 16 / scaleFactor;

  // For each vertex, push label away from centroid
  let dirX = verts.A.x - centX;
  let dirY = verts.A.y - centY;
  let len = sqrt(dirX * dirX + dirY * dirY);

  text('A', verts.A.x + labelDist * dirX / len,
           verts.A.y + labelDist * dirY / len);
}

Angle Labels

Position angle measurements along the angle bisector:

function drawAngleLabel(vertex, adj1, adj2, angleDeg, scaleFactor) {
  let a1 = atan2(adj1.y - vertex.y, adj1.x - vertex.x);
  let a2 = atan2(adj2.y - vertex.y, adj2.x - vertex.x);

  // Normalize and find midpoint angle
  // ... angle normalization code ...

  let midAngle = (a1 + a2) / 2;
  let labelDist = 35 / scaleFactor;

  let labelX = vertex.x + labelDist * cos(midAngle);
  let labelY = vertex.y + labelDist * sin(midAngle);
  text(angleDeg + "°", labelX, labelY);
}

Common Patterns

Multiple Panels with Shapes

When displaying multiple shapes in panels (like triangle classifications):

function drawPanels() {
  let panelWidth = (canvasWidth - 80) / 3;
  let startX = 30;

  for (let i = 0; i < 3; i++) {
    let x = startX + i * (panelWidth + 10);
    let centerX = x + panelWidth / 2;
    let centerY = panelY + 130;

    // Each shape draws in its own coordinate system
    drawShapePanel(centerX, centerY, shapes[i], panelWidth);
  }
}

Right Angle Indicators

function drawRightAngle(vertex, adj1, adj2, scaleFactor) {
  let size = 12 / scaleFactor;
  let dir1 = atan2(adj1.y - vertex.y, adj1.x - vertex.x);
  let dir2 = atan2(adj2.y - vertex.y, adj2.x - vertex.x);

  let p1 = { x: vertex.x + size * cos(dir1), y: vertex.y + size * sin(dir1) };
  let p2 = { x: vertex.x + size * cos(dir2), y: vertex.y + size * sin(dir2) };
  let corner = {
    x: vertex.x + size * cos(dir1) + size * cos(dir2),
    y: vertex.y + size * sin(dir1) + size * sin(dir2)
  };

  line(p1.x, p1.y, corner.x, corner.y);
  line(p2.x, p2.y, corner.x, corner.y);
}

Checklist for Geometry MicroSims

Before finalizing a geometry MicroSim, verify:

File Structure

  • HTML file is named main.html
  • Directory follows docs/sims/[sim-name]/ pattern
  • index.md uses main.html in iframe src
  • Screenshot image exists for preview

Geometry Code Quality

  • Shapes draw in local coordinates (centered at origin)
  • Push/pop/translate/scale used for positioning
  • Stroke weights compensated for scale factor
  • Text sizes compensated for scale factor
  • Vertex labels positioned radially from centroid
  • Angle measurements match the actual drawn angles
  • Geometry calculations use mathematically correct formulas
  • Labels stay associated with correct elements through transforms
  • Bounding box calculated for auto-fit scaling

Anti-Patterns to Avoid

Manual Offset Calculations

Bad:

// Scattered offsets throughout code
let ax = cx - baseLen / 2 + offsetX + panelX;
let ay = cy + verticalOffset + panelY - 10;

Good:

// Clean local coordinates with transform
push();
translate(panelCenterX, panelCenterY);
let ax = -baseLen / 2;
let ay = 0;
// ...
pop();

Rotation Breaking Label Associations

Bad:

// Rotate vertices but keep same angle labels
rotateVertices();
drawAngle(verts.A, angleA);  // angleA no longer at vertex A!

Good:

// Calculate vertices directly from angles
let verts = calculateFromAngles(angles);
// verts.A always has angles[0]

Mathematical Accuracy

For geometry education, mathematical accuracy is critical:

  1. Use law of sines/cosines for triangle calculations
  2. Verify angle sums (triangle: 180°, quadrilateral: 360°)
  3. Test with known values (30-60-90, 45-45-90 triangles)
  4. Display angles that match the actual geometric shape
  • microsim-generator/references/p5-guide.md - General p5.js MicroSim patterns
  • microsim-utils - Quality validation and standardization

Examples

See these MicroSims for implementation examples:

  • docs/sims/triangle-classification-angles/ - Triangle types by angles
  • docs/sims/triangle-classification-sides/ - Triangle types by sides (if exists)
  • Other geometry sims in docs/sims/

Score

Total Score

70/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon