npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

circle-human-detector

v1.0.0

Published

Behavioral analysis of circle-drawing interactions to estimate whether they were produced by a human or a machine. Pure analysis engine plus an optional browser pointer-event collector.

Readme

circle-human-detector

Behavioral analysis of circle-drawing interactions: estimates whether a "draw a circle" gesture was produced by a human hand or a machine, from pointer movement dynamics rather than geometric perfection.

What this is: a probabilistic behavioral signal — one input among several you might use in a bot-mitigation stack.

What this is not: proof of humanity, a CAPTCHA solver, or a replacement for attestation-based verification. A geometrically imperfect circle does not prove a human drew it; a machine that models human dynamics can evade any behavioral check. See docs/LIMITATIONS.md before deploying.

  • TypeScript-first, strict types, zero runtime dependencies
  • Dual ESM + CommonJS builds with generated .d.ts
  • Browser collector (mouse / touch / pen via Pointer Events) and a pure, Node-compatible analysis engine
  • Deterministic, explainable verdicts: every result carries the metric table and anomaly list that produced it
  • No data collection, no network calls, no telemetry — samples stay in memory until you decide what to do with them

Install

npm install circle-human-detector

Quick start (browser)

import { CircleDetector } from "circle-human-detector";

const detector = new CircleDetector({
  target: document.querySelector<HTMLCanvasElement>("#draw-surface"),
  sensitivity: "balanced",
});

detector.start();

// The user draws a circle with mouse, finger, or stylus...

canvas.addEventListener("pointerup", () => {
  const result = detector.analyze();
  console.log(result.classification); // "HUMAN_LIKE" | "SUSPICIOUS" | "INCONCLUSIVE"
  console.log(result.score);          // 0..1 human-likelihood
  console.log(result.confidence);      // 0..1 confidence in the verdict
  console.log(result.anomalies);       // machine signatures / data-quality notes
});

The drawing surface needs touch-action: none so touch strokes are not consumed by scrolling:

#draw-surface { touch-action: none; }

A complete working demo lives in examples/browser. Serve from the repo root (the example imports ../../dist/esm/index.js, which only resolves when the package root is the docroot):

npm run build
python3 -m http.server 8000   # run from the repo root
# open http://localhost:8000/examples/browser/index.html

For a self-contained interactive playground — one file, opens straight from disk, no server, no network — build the demo page:

npm run build:demo
# then open examples/browser/demo.html in a browser

Draw a circle with mouse, touch, or pen, or replay scripted/noisy synthetic bots, and watch the verdict, detected signals, and per-metric contributions update. The engine is inlined into the page by scripts/build-demo.mjs, so the demo always reflects the current build.

Two more runnable flows ship with the examples:

  • Offline capture for real datasets: examples/browser/collect.html — consent-gated, zero network, exports labeled JSON per drawing. The collection plan is in docs/CAPTURE-CAMPAIGN.md.
  • Server-side verification with replay protection: examples/node/challenge-server.mjs — single-use challenge tokens, wall-clock window binding, analysis on the server. The concrete version of the /api/challenge pseudo-code below.

Quick start (Node, pure analysis)

The analysis engine has no DOM dependency. Analyze samples collected elsewhere — for example, submitted by a browser client to your API:

import { analyzeSamples } from "circle-human-detector/analyze";

app.post("/api/challenge", (req, res) => {
  const result = analyzeSamples(req.body.samples, {
    sensitivity: "balanced",
  });

  if (result.classification === "HUMAN_LIKE" && result.confidence > 0.6) {
    res.json({ pass: true });
  } else if (result.classification === "SUSPICIOUS") {
    res.json({ pass: false, reason: "suspicious drawing behavior" });
  } else {
    // INCONCLUSIVE: too little or too ambiguous data. Ask the user to
    // retry, or fall back to another challenge. Never treat it as an
    // accusation.
    res.json({ pass: false, reason: "inconclusive, please retry" });
  }
});

A runnable version is in examples/node/analyze-demo.mjs. For the full verification flow — single-use challenge tokens, wall-clock window binding, replay rejection — run examples/node/challenge-server.mjs:

npm run build
node examples/node/challenge-server.mjs
# open http://localhost:8787

The result object

interface CircleDetectionResult {
  /** Human-likelihood in [0, 1]. A heuristic signal, not a probability. */
  score: number;
  /** Confidence in [0, 1]. Below the sensitivity preset's minimum the
   *  verdict is INCONCLUSIVE by construction. */
  confidence: number;
  classification: "HUMAN_LIKE" | "SUSPICIOUS" | "INCONCLUSIVE";
  /** Machine signatures and data-quality findings, with details. */
  anomalies: Anomaly[];
  /** Raw behavioral metrics for transparency and downstream tuning. */
  metrics: BehavioralMetrics;
  /** Per-metric human-likeness values and weights behind `score`. */
  contributions: MetricContribution[];
  /** Whether the data was sufficient to attempt classification. */
  dataQuality: DataQuality;
}

Classification semantics:

| Classification | Meaning | | --- | --- | | HUMAN_LIKE | Movement dynamics are consistent with a human. Probabilistic evidence, never proof. | | SUSPICIOUS | Machine signatures present, or dynamics strongly inconsistent with natural movement. | | INCONCLUSIVE | Insufficient data, ambiguous metrics, or a shape that is not circle-like. The detector refuses to guess. |

Sensitivity presets

| Preset | HUMAN_LIKE at | SUSPICIOUS at or below | Minimum confidence | | --- | --- | --- | --- | | relaxed | score >= 0.48 | 0.26 | 0.30 | | balanced | score >= 0.58 | 0.33 | 0.40 | | strict | score >= 0.66 | 0.40 | 0.50 |

Scores between the two thresholds are the gray zone: INCONCLUSIVE. All thresholds are overridable (thresholds option), along with the data floors (minSamples = 24, minDurationMs = 300, minCoverageTurns = 0.85) and the pause threshold (pauseMs = 120). See docs/API.md for the full list.

How detection works

Two independent evidence channels, both visible in every result:

  1. Weighted human-likeness metrics. Angular-velocity variation, systematic speed modulation across the sweep, low-frequency radius wobble, start/end speed ramps, tremor amplitude, and more. Each is normalized to 0..1 through explicit, inspectable piecewise-linear curves and combined by weight. Amplitude-only metrics are blended with their temporal autocorrelation, because injected white noise fakes amplitude but not structure.

  2. Machine-signature anomalies with penalties. Hard signatures no natural input pipeline produces: exactly-constant angular velocity, collinear interpolation runs, piecewise-constant speed, uniform timestamps, zero radial residual, jitter amplitude without temporal structure, and broadband radius wander whose coupling into speed betrays a constant-omega generator. Each subtracts a fixed penalty from the score.

Geometric imperfection alone never proves anything: a noisy circle with machine-consistent dynamics still classifies SUSPICIOUS, and a clean circle drawn by a skilled human still passes on its dynamics. The confidence term combines data sufficiency with cross-metric agreement — when metrics disagree, the verdict is INCONCLUSIVE rather than a guess.

The full metric reference, weights, and anomaly catalog are in docs/API.md.

Benchmark

npm run evaluate

Runs the engine against a 200-trial-per-class randomized synthetic corpus (seeded, reproducible) and gates the result:

| Class | HUMAN_LIKE | SUSPICIOUS | INCONCLUSIVE | Mean score | Mean conf | | --- | --- | --- | --- | --- | --- | | Human movement model | 100% | 0% | 0% | 0.965 | 0.690 | | Parametric generator | 0% | 100% | 0% | 0.000 | 0.783 | | Noisy generator | 0% | 100% | 0% | 0.018 | 0.561 | | Waypoint interpolation | 0% | 100% | 0% | 0.000 | 0.787 | | Adversarial generator | 0% | 2.5% | 97.5% | 0.253 | 0.382 | | Straight line (not a circle) | 0% | 0% | 100% | 0.000 | 0.000 |

These numbers validate the engine against modeled humans and generators — a regression gate, not a real-world accuracy measurement. The same command also prints an informational physics-law audit: each corpus's two-thirds power-law slope, signal-dependent-noise exponent, and movement smoothness, measured against published motor-control targets. To evaluate against real captures, collect them with examples/browser/collect.html — a consent-gated page that runs from file:// and makes zero network requests — and run npm run evaluate:real -- <folder> for per-preset per-label confusion matrices plus the same physics audit over your labeled captures. Methodology, and how to replace the synthetic corpus with consented real captures: docs/EVALUATION.md.

Privacy and consent

  • The library collects nothing on its own: samples exist in memory, for as long as you hold them.
  • It performs no network I/O and ships no telemetry.
  • If you transmit drawing data anywhere (e.g., to your API for server-side analysis), that is your policy decision: disclose it and obtain consent. Pointer traces can be biometric-adjacent data (movement characteristics), so treat them accordingly.
  • See docs/LIMITATIONS.md.

Development

npm run typecheck   # strict tsc over src + test + scripts + examples/node
npm run build       # dist/esm + dist/cjs + declarations
npm test            # unit + integration suite (node:test, zero deps)
npm run evaluate    # synthetic-corpus benchmark with gates
npm run evaluate:real -- <folder>  # labeled real-capture report

Requirements: Node >= 24 for development (TypeScript type stripping for tests); the published package runs on Node >= 18 and any modern browser.

License

MIT — see LICENSE.

Created by flawme.