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

cliffy-tsukoshi

v0.1.0

Published

Minimal geometric state management - pure TypeScript, zero dependencies

Downloads

109

Readme

cliffy-tsukoshi

Minimal geometric state management for JavaScript/TypeScript. Pure TypeScript, zero dependencies, works everywhere including React Native.

Features

  • GeometricState - Smooth state interpolation via .blend()
  • Rotor - Rotation representation with SLERP interpolation
  • Transform - Combined rotation + translation
  • Zero dependencies - Pure TypeScript math
  • Universal - Works in browser, Node.js, React Native, Deno

Installation

npm install cliffy-tsukoshi

Quick Start

import { GeometricState, Rotor, Transform } from 'cliffy-tsukoshi';

// Create position state
const position = GeometricState.fromVector(100, 200, 0);

// Smooth interpolation (the key feature!)
const target = GeometricState.fromVector(300, 400, 0);
const smoothed = position.blend(target, 0.15); // Move 15% toward target

// Apply rotation (45 degrees in XY plane)
const rotation = Rotor.xy(Math.PI / 4);
const rotated = position.applyRotor(rotation);

// Full transform (rotation + translation)
const transform = Transform.new(
  Rotor.xz(Math.PI / 6),
  { x: 10, y: 20, z: 0 }
);
const transformed = position.applyTransform(transform);

// Extract values
const [x, y, z] = smoothed.asVector();
console.log(`Position: (${x}, ${y}, ${z})`);

Core Concepts

GeometricState

The main container for position/state data. Internally uses GA3 (3D Geometric Algebra) but exposes a simple vector interface.

// Create states
const pos = GeometricState.fromVector(x, y, z);
const pos2D = GeometricState.fromVector2D(x, y);
const value = GeometricState.fromScalar(42);

// Smooth interpolation - the killer feature
// In a game loop: position = position.blend(target, 0.1)
const smoothed = current.blend(target, t);

// Arithmetic
const sum = a.add(b);
const diff = a.sub(b);
const scaled = a.scale(2);

// Extract values
const [x, y, z] = state.asVector();
const { x, y, z } = state.asVectorObject();
const scalar = state.asScalar();

Rotor (Rotations)

Rotors represent rotations without gimbal lock. They support smooth interpolation via SLERP.

// Create rotations
const r1 = Rotor.xy(angle);  // Around Z axis
const r2 = Rotor.xz(angle);  // Around Y axis
const r3 = Rotor.yz(angle);  // Around X axis
const r4 = Rotor.fromAxisAngle(ax, ay, az, angle);

// Apply to state
const rotated = position.applyRotor(rotation);

// Compose rotations
const combined = r1.then(r2);  // r1 first, then r2

// Interpolate (SLERP)
const halfway = r1.slerpTo(r2, 0.5);

Transform (Rotation + Translation)

Combines rotation and translation into a single operation.

// Create transforms
const t = Transform.new(rotor, { x: 10, y: 20, z: 0 });
const tRotate = Transform.rotation(rotor);
const tMove = Transform.fromTranslation({ x: 10, y: 0, z: 0 });

// Apply
const transformed = position.applyTransform(t);

// Compose
const combined = t1.then(t2);

// Interpolate
const halfway = t1.interpolateTo(t2, 0.5);

ReactiveState

A mutable container with subscriptions for reactive updates.

import { reactiveState, GeometricState } from 'cliffy-tsukoshi';

const position = reactiveState(GeometricState.fromVector(0, 0, 0));

// Subscribe to changes
position.subscribe((state) => {
  const [x, y, z] = state.asVector();
  updateUI(x, y, z);
});

// Update (triggers subscribers)
position.set(GeometricState.fromVector(100, 200, 0));

// Smooth update
position.blendTo(target, 0.1);

Common Patterns

Smooth Following (Game Loop)

function gameLoop() {
  // Smooth follow: move 10% toward target each frame
  playerPosition = playerPosition.blend(targetPosition, 0.1);

  // Smooth rotation
  playerRotation = playerRotation.slerpTo(targetRotation, 0.1);

  render();
  requestAnimationFrame(gameLoop);
}

2D Game Movement

const position = GeometricState.fromVector2D(100, 100);
const velocity = GeometricState.fromVector2D(5, 0);

function update() {
  // Move
  position = position.add(velocity);

  // Rotate velocity (turn)
  const turn = Rotor.xy(turnAngle);
  velocity = velocity.applyRotor(turn);
}

React Native Integration

import { useRef, useEffect } from 'react';
import { Animated } from 'react-native';
import { GeometricState } from 'cliffy-tsukoshi';

function useGeometricAnimation(target: GeometricState) {
  const position = useRef(GeometricState.zero());
  const animX = useRef(new Animated.Value(0)).current;
  const animY = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const interval = setInterval(() => {
      position.current = position.current.blend(target, 0.1);
      const [x, y] = position.current.asVector2D();
      animX.setValue(x);
      animY.setValue(y);
    }, 16);

    return () => clearInterval(interval);
  }, [target]);

  return { x: animX, y: animY };
}

More Examples

Camera Following with Damping

class SmoothCamera {
  position: GeometricState;
  zoom: GeometricState;

  constructor() {
    this.position = GeometricState.fromVector2D(0, 0);
    this.zoom = GeometricState.fromScalar(1);
  }

  follow(target: GeometricState, deltaTime: number) {
    // Smooth follow with frame-rate independent damping
    const smoothing = 1 - Math.pow(0.001, deltaTime);
    this.position = this.position.blend(target, smoothing);
  }

  zoomTo(level: number, speed: number = 0.1) {
    const targetZoom = GeometricState.fromScalar(level);
    this.zoom = this.zoom.blend(targetZoom, speed);
  }

  getViewMatrix(): { x: number; y: number; scale: number } {
    const [x, y] = this.position.asVector2D();
    return { x: -x, y: -y, scale: this.zoom.asScalar() };
  }
}

Touch/Joystick Input

class JoystickController {
  private input = GeometricState.zero();
  private smoothedInput = GeometricState.zero();

  // Called on touch/mouse move
  setInput(dx: number, dy: number) {
    // Clamp to unit circle
    const mag = Math.sqrt(dx * dx + dy * dy);
    if (mag > 1) {
      dx /= mag;
      dy /= mag;
    }
    this.input = GeometricState.fromVector2D(dx, dy);
  }

  // Called on touch/mouse up
  release() {
    this.input = GeometricState.zero();
  }

  // Called every frame
  update(): [number, number] {
    // Smooth the input to avoid jerky movement
    this.smoothedInput = this.smoothedInput.blend(this.input, 0.2);
    return this.smoothedInput.asVector2D();
  }
}

Particle System

interface Particle {
  position: GeometricState;
  velocity: GeometricState;
  life: number;
}

class ParticleEmitter {
  particles: Particle[] = [];
  origin: GeometricState;

  constructor(x: number, y: number) {
    this.origin = GeometricState.fromVector2D(x, y);
  }

  emit(count: number) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = 50 + Math.random() * 100;

      this.particles.push({
        position: this.origin.clone(),
        velocity: GeometricState.fromVector2D(
          Math.cos(angle) * speed,
          Math.sin(angle) * speed
        ),
        life: 1.0,
      });
    }
  }

  update(dt: number) {
    const gravity = GeometricState.fromVector2D(0, 200);
    const drag = 0.98;

    this.particles = this.particles.filter(p => {
      // Apply physics
      p.velocity = p.velocity.add(gravity.scale(dt)).scale(drag);
      p.position = p.position.add(p.velocity.scale(dt));
      p.life -= dt;

      return p.life > 0;
    });
  }
}

Animated Sprite with Rotation

class AnimatedSprite {
  position: GeometricState;
  rotation: Rotor;
  targetRotation: Rotor;

  constructor(x: number, y: number) {
    this.position = GeometricState.fromVector2D(x, y);
    this.rotation = Rotor.identity();
    this.targetRotation = Rotor.identity();
  }

  lookAt(targetX: number, targetY: number) {
    const [x, y] = this.position.asVector2D();
    const angle = Math.atan2(targetY - y, targetX - x);
    this.targetRotation = Rotor.xy(angle);
  }

  update() {
    // Smooth rotation toward target
    this.rotation = this.rotation.slerpTo(this.targetRotation, 0.15);
  }

  getAngle(): number {
    return this.rotation.angle();
  }
}

Physics Body with Velocity Damping

class PhysicsBody {
  position: GeometricState;
  velocity: GeometricState;

  readonly mass: number;
  readonly drag: number;

  constructor(x: number, y: number, mass = 1, drag = 0.02) {
    this.position = GeometricState.fromVector2D(x, y);
    this.velocity = GeometricState.zero();
    this.mass = mass;
    this.drag = drag;
  }

  applyForce(fx: number, fy: number) {
    const acceleration = GeometricState.fromVector2D(fx / this.mass, fy / this.mass);
    this.velocity = this.velocity.add(acceleration);
  }

  applyImpulse(ix: number, iy: number) {
    this.velocity = this.velocity.add(GeometricState.fromVector2D(ix, iy));
  }

  update(dt: number) {
    // Apply drag
    this.velocity = this.velocity.scale(1 - this.drag);

    // Update position
    this.position = this.position.add(this.velocity.scale(dt));

    // Stop if very slow
    if (this.velocity.magnitude() < 0.01) {
      this.velocity = GeometricState.zero();
    }
  }
}

Path Following

class PathFollower {
  private waypoints: GeometricState[];
  private currentIndex = 0;
  private position: GeometricState;

  constructor(waypoints: Array<[number, number]>) {
    this.waypoints = waypoints.map(([x, y]) => GeometricState.fromVector2D(x, y));
    this.position = this.waypoints[0].clone();
  }

  update(speed: number = 0.05): [number, number] {
    if (this.waypoints.length === 0) {
      return this.position.asVector2D();
    }

    const target = this.waypoints[this.currentIndex];
    this.position = this.position.blend(target, speed);

    // Check if reached waypoint
    if (this.position.distance(target) < 1) {
      this.currentIndex = (this.currentIndex + 1) % this.waypoints.length;
    }

    return this.position.asVector2D();
  }

  getProgress(): number {
    return this.currentIndex / this.waypoints.length;
  }
}

UI Progress/Health Bar

class SmoothProgressBar {
  private displayValue: GeometricState;
  private actualValue: number;

  constructor(initial: number = 1) {
    this.actualValue = initial;
    this.displayValue = GeometricState.fromScalar(initial);
  }

  setValue(value: number) {
    this.actualValue = Math.max(0, Math.min(1, value));
  }

  update(): number {
    const target = GeometricState.fromScalar(this.actualValue);
    this.displayValue = this.displayValue.blend(target, 0.1);
    return this.displayValue.asScalar();
  }

  // For damage flash effects
  flash(amount: number) {
    // Instantly show damage, then smooth back
    this.displayValue = GeometricState.fromScalar(
      this.displayValue.asScalar() - amount
    );
  }
}

Pan and Zoom Controls

class PanZoomController {
  offset: GeometricState;
  zoom: GeometricState;

  private targetOffset: GeometricState;
  private targetZoom: GeometricState;

  constructor() {
    this.offset = GeometricState.zero();
    this.targetOffset = GeometricState.zero();
    this.zoom = GeometricState.fromScalar(1);
    this.targetZoom = GeometricState.fromScalar(1);
  }

  pan(dx: number, dy: number) {
    const scale = this.zoom.asScalar();
    this.targetOffset = this.targetOffset.add(
      GeometricState.fromVector2D(dx / scale, dy / scale)
    );
  }

  zoomAt(factor: number, centerX: number, centerY: number) {
    const currentZoom = this.targetZoom.asScalar();
    const newZoom = Math.max(0.1, Math.min(10, currentZoom * factor));
    this.targetZoom = GeometricState.fromScalar(newZoom);

    // Zoom toward cursor position
    const [ox, oy] = this.targetOffset.asVector2D();
    const zoomRatio = newZoom / currentZoom;
    this.targetOffset = GeometricState.fromVector2D(
      centerX - (centerX - ox) * zoomRatio,
      centerY - (centerY - oy) * zoomRatio
    );
  }

  update() {
    this.offset = this.offset.blend(this.targetOffset, 0.15);
    this.zoom = this.zoom.blend(this.targetZoom, 0.15);
  }

  screenToWorld(screenX: number, screenY: number): [number, number] {
    const [ox, oy] = this.offset.asVector2D();
    const scale = this.zoom.asScalar();
    return [(screenX - ox) / scale, (screenY - oy) / scale];
  }
}

React Hook for Smooth Values

import { useState, useEffect, useRef } from 'react';
import { GeometricState } from 'cliffy-tsukoshi';

function useSmoothValue(target: number, smoothing: number = 0.1): number {
  const [display, setDisplay] = useState(target);
  const state = useRef(GeometricState.fromScalar(target));

  useEffect(() => {
    let animationId: number;
    const targetState = GeometricState.fromScalar(target);

    function animate() {
      state.current = state.current.blend(targetState, smoothing);
      setDisplay(state.current.asScalar());

      if (Math.abs(state.current.asScalar() - target) > 0.001) {
        animationId = requestAnimationFrame(animate);
      }
    }

    animationId = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(animationId);
  }, [target, smoothing]);

  return display;
}

// Usage
function Counter({ value }: { value: number }) {
  const smoothValue = useSmoothValue(value);
  return <span>{Math.round(smoothValue)}</span>;
}

Multiplayer State Interpolation

interface NetworkState {
  position: GeometricState;
  timestamp: number;
}

class InterpolatedPlayer {
  private buffer: NetworkState[] = [];
  private renderPosition: GeometricState;

  // Render 100ms behind to allow interpolation
  private readonly INTERPOLATION_DELAY = 100;

  constructor(initialX: number, initialY: number) {
    this.renderPosition = GeometricState.fromVector2D(initialX, initialY);
  }

  // Called when network update arrives
  receiveState(x: number, y: number, serverTime: number) {
    this.buffer.push({
      position: GeometricState.fromVector2D(x, y),
      timestamp: serverTime,
    });

    // Keep only recent states
    const cutoff = serverTime - 1000;
    this.buffer = this.buffer.filter(s => s.timestamp > cutoff);
  }

  // Called every frame
  update(currentTime: number): [number, number] {
    const renderTime = currentTime - this.INTERPOLATION_DELAY;

    // Find states to interpolate between
    let before: NetworkState | null = null;
    let after: NetworkState | null = null;

    for (const state of this.buffer) {
      if (state.timestamp <= renderTime) {
        before = state;
      } else if (!after) {
        after = state;
      }
    }

    if (before && after) {
      // Interpolate between the two states
      const t = (renderTime - before.timestamp) / (after.timestamp - before.timestamp);
      this.renderPosition = before.position.blend(after.position, t);
    } else if (before) {
      // Extrapolate slightly
      this.renderPosition = this.renderPosition.blend(before.position, 0.1);
    }

    return this.renderPosition.asVector2D();
  }
}

Why Geometric Algebra?

Under the hood, cliffy-tsukoshi uses GA3 (Clifford Algebra Cl(3,0)). This provides:

  1. Unified representation - Scalars, vectors, rotations all in one type
  2. No gimbal lock - Rotors avoid quaternion edge cases
  3. Composable - Rotations compose correctly via geometric product
  4. Interpolation - SLERP "just works" for smooth rotation blending

You don't need to understand GA to use the library - the API is designed around familiar vector operations.

API Reference

GeometricState

| Method | Description | |--------|-------------| | fromVector(x, y, z) | Create from 3D coordinates | | fromVector2D(x, y) | Create from 2D coordinates | | fromScalar(n) | Create from scalar value | | blend(target, t) | Interpolate toward target | | add(other) | Vector addition | | sub(other) | Vector subtraction | | scale(factor) | Scalar multiplication | | applyRotor(r) | Apply rotation | | applyTransform(t) | Apply rotation + translation | | asVector() | Extract as [x, y, z] | | asVector2D() | Extract as [x, y] | | asScalar() | Extract scalar value | | distance(other) | Euclidean distance | | magnitude() | Vector length |

Rotor

| Method | Description | |--------|-------------| | xy(angle) | Rotation in XY plane (around Z) | | xz(angle) | Rotation in XZ plane (around Y) | | yz(angle) | Rotation in YZ plane (around X) | | fromAxisAngle(ax, ay, az, angle) | From axis-angle | | identity() | No rotation | | transform(v) | Apply to multivector | | then(other) | Compose rotations | | inverse() | Reverse rotation | | slerp(t) | Interpolate from identity | | slerpTo(other, t) | Interpolate to other | | angle() | Get rotation angle |

Transform

| Method | Description | |--------|-------------| | new(rotor, translation) | Create from components | | identity() | No transformation | | rotation(rotor) | Pure rotation | | fromTranslation(t) | Pure translation | | apply(v) | Apply to multivector | | then(other) | Compose transforms | | inverse() | Reverse transform | | interpolateTo(other, t) | Interpolate to other |

License

MIT

Part of Cliffy

cliffy-tsukoshi is the pure TypeScript extraction of geometric state management from Cliffy, a framework for building collaborative applications using geometric algebra.