gokul/flex-led-wristband

A flexible, rechargeable touch wristband PCB with MCP73831 Li-ion charging, switched 3.0 V regulation, ATtiny402 control, TTP223 capacitive touch sensing, and six charlieplexed red LEDs plus power/charge indicators.

Version
0.0.3
License
unset
Stars
0

lib/outline.ts

type Point = { x: number; y: number };

// Circular one-millimetre fillets, discretised to at most six degrees per chord.
// This includes the concave shoulder corners, where a flex outline can tear.
function roundCorners(vertices: Point[], radius: number): Point[] {
  return vertices.flatMap((vertex, index) => {
    const previous = vertices[(index + vertices.length - 1) % vertices.length];
    const next = vertices[(index + 1) % vertices.length];
    const previousLength = Math.hypot(
      previous.x - vertex.x,
      previous.y - vertex.y,
    );
    const nextLength = Math.hypot(next.x - vertex.x, next.y - vertex.y);
    const previousDirection = {
      x: (previous.x - vertex.x) / previousLength,
      y: (previous.y - vertex.y) / previousLength,
    };
    const nextDirection = {
      x: (next.x - vertex.x) / nextLength,
      y: (next.y - vertex.y) / nextLength,
    };
    const angle = Math.acos(
      previousDirection.x * nextDirection.x +
        previousDirection.y * nextDirection.y,
    );
    const tangentDistance = Math.min(
      radius / Math.tan(angle / 2),
      previousLength / 3,
      nextLength / 3,
    );
    const actualRadius = tangentDistance * Math.tan(angle / 2);
    const bisectorLength = Math.hypot(
      previousDirection.x + nextDirection.x,
      previousDirection.y + nextDirection.y,
    );
    const centerDistance = actualRadius / Math.sin(angle / 2);
    const center = {
      x:
        vertex.x +
        ((previousDirection.x + nextDirection.x) * centerDistance) /
          bisectorLength,
      y:
        vertex.y +
        ((previousDirection.y + nextDirection.y) * centerDistance) /
          bisectorLength,
    };
    const startAngle = Math.atan2(
      vertex.y + previousDirection.y * tangentDistance - center.y,
      vertex.x + previousDirection.x * tangentDistance - center.x,
    );
    const endAngle = Math.atan2(
      vertex.y + nextDirection.y * tangentDistance - center.y,
      vertex.x + nextDirection.x * tangentDistance - center.x,
    );
    let sweep = endAngle - startAngle;
    while (sweep > Math.PI) sweep -= 2 * Math.PI;
    while (sweep < -Math.PI) sweep += 2 * Math.PI;
    const segments = Math.ceil(Math.abs(sweep) / (Math.PI / 30));
    return Array.from({ length: segments + 1 }, (_, step) => ({
      x:
        center.x +
        actualRadius * Math.cos(startAngle + (sweep * step) / segments),
      y:
        center.y +
        actualRadius * Math.sin(startAngle + (sweep * step) / segments),
    }));
  });
}

export const boardOutline = roundCorners(
  [
    { x: -45, y: -5 },
    { x: -17, y: -5 },
    { x: -15, y: -16 },
    { x: 15, y: -16 },
    { x: 17, y: -5 },
    { x: 45, y: -5 },
    { x: 45, y: 5 },
    { x: 17, y: 5 },
    { x: 15, y: 16 },
    { x: -15, y: 16 },
    { x: -17, y: 5 },
    { x: -45, y: 5 },
  ],
  1,
);