polygon drawing (splatter balls)

by Ben Gillbanks

HTML

<div class="row">
    <canvas id="canvas" width="32" height="32"></canvas>
    </div>
    <div class="row">
    <label for="polygonInput">Load Polygon:</label><br />
    <input type="text" id="polygonInput" placeholder="polygon array">
    </div>
    <div class="row">
    <button id="loadPolygonButton">Load</button>
    <button id="clearButton">Clear</button>
    <button id="rotateButton">Rotate</button>
    </div>

CSS

body {
    padding: 10px;
    font-family: arial, san-serif;
    font-size: 16px;
}

button {
    padding: 2px 12px;
}

#canvas {
    border: 1px solid #000;
    width: 320px;
    height: 320px;
    image-rendering: pixelated;
}

#polygonInput {
    width: 320px;
}

div.row {
    margin-bottom: 10px;
}

JavaScript

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let points = [];
const gridStep = 4;   // draw grid every 4
const snapStep = 1;   // snap points to 1
const scale = 10;   // how many real pixels per logical unit
const logicalSize = 32; // your world is 32x32
const WORLD_SIZE = 32;
const ORIGIN = WORLD_SIZE / 2;

canvas.width = logicalSize * scale;
canvas.height = logicalSize * scale;

ctx.setTransform(scale, 0, 0, scale, 0, 0);
// Disable antialiasing
ctx.imageSmoothingEnabled = false;

const hitRadius = 4; // logical units (not pixels)
let dragIndex = -1;

const inputEl = document.getElementById('polygonInput');

function getSignedArea(vertices) {
	let area = 0;

	for (let i = 0; i < vertices.length; i++) {
		const [x1, y1] = vertices[i];
		const [x2, y2] = vertices[(i + 1) % vertices.length];
		area += (x1 * y2) - (x2 * y1);
	}

	return area / 2;
}

function ensureCounterClockwise(vertices) {
	if (vertices.length < 3) return vertices;

	return getSignedArea(vertices) < 0
		? [...vertices].reverse()
		: vertices;
}

function clamp(v, min, max) {
	return Math.max(min, Math.min(max, v));
}

function closestPointOnSegment(px, py, ax, ay, bx, by) {
	const abx = bx - ax;
	const aby = by - ay;
	const apx = px - ax;
	const apy = py - ay;

	const abLen2 = abx * abx + aby * aby;
	if (abLen2 === 0) {
		return { x: ax, y: ay, t: 0, d2: (px - ax) ** 2 + (py - ay) ** 2 };
	}

	let t = (apx * abx + apy * aby) / abLen2;
	t = clamp(t, 0, 1);

	const cx = ax + t * abx;
	const cy = ay + t * aby;

	const dx = px - cx;
	const dy = py - cy;

	return { x: cx, y: cy, t, d2: dx * dx + dy * dy };
}

function findClosestEdgeIndex(x, y, maxDist) {
	if (points.length < 2) return -1;

	const maxD2 = maxDist * maxDist;
	let bestEdgeStart = -1;
	let bestD2 = maxD2;

	for (let i = 0; i < points.length; i++) {
		const a = points[i];
		const b = points[(i + 1) % points.length]; //...