tea-web

Browser runtime for Kit TEA applications

This package enables Kit TEA applications to run in web browsers using HTML5 Canvas for rendering and DOM events for input.

Files

FileDescription
.editorconfigEditor formatting configuration
.gitignoreGit ignore rules for build artifacts and dependencies
.tool-versionsasdf tool versions (Zig, Kit)
examples/kit-counter/counter.kitExample: Kit TEA counter
examples/README.mdExamples documentation
kit.tomlPackage manifest with metadata and dependencies
package.jsonnpm package manifest
README.mdThis file
src/diff.jsElement diffing for optimized rendering
src/events.d.tsTypeScript declarations for event capture
src/events.jsDOM event capture and Kit TEA Msg conversion
src/hmr.jsHot Module Replacement for development
src/html-widgets.jsNative HTML form elements overlay
src/index.d.tsTypeScript declarations for main module
src/index.jsMain module and public API
src/loader.d.tsTypeScript declarations for WASM loader
src/loader.jsWASM interpreter loader
src/main.kitMain module
src/renderer.d.tsTypeScript declarations for canvas renderer
src/renderer.jsCanvas 2D renderer for Kit TEA elements
src/runtime.jsMain loop, event processing, and rendering runtime
src/webgl-renderer.jsWebGL renderer for high-performance scenes

Features

  • Canvas 2D Renderer - Renders all Kit TEA Element types to Canvas
  • Event Capture - Mouse and keyboard events mapped to Kit Msg format
  • requestAnimationFrame Loop - Smooth 60fps rendering with proper timing
  • WASM Integration - Works with Kit's WASM interpreter (optional)
  • Pure JavaScript Mode - Can run TEA apps without WASM for prototyping

Installation

npm install kit-tea-web

Or include directly:

<script type="module">
  import { runTeaApp } from './kit-tea-web/src/index.js';
</script>

Quick Start

Pure JavaScript TEA App

import { runTeaApp } from 'kit-tea-web';

const canvas = document.getElementById('app');

runTeaApp({
  canvas,

  // Initialize model
  init: () => ({ count: 0 }),

  // Update model based on messages
  update: (msg, model) => {
    if (msg.kind === 'key-pressed' && msg.key === 265) { // Up arrow
      return { count: model.count + 1 };
    }
    return model;
  },

  // Render view from model
  view: (model) => ({
    type: 'Group',
    elements: [
      { type: 'Rect-Fill', x: 0, y: 0, w: 400, h: 300, color: 0x1a1a2eff },
      { type: 'Text', x: 180, y: 130, size: 48, color: 0xffd700ff, content: String(model.count) }
    ]
  })
});

With Kit WASM

Run Kit source code directly in the browser using the WASM interpreter:

import { loadKitWasm, createTeaApp } from 'kit-tea-web';
import { renderElement } from 'kit-tea-web/renderer';

// Load Kit source (use \$ to escape string interpolation in JS template literals)
const kitSource = `
init = fn() => {count: 0}
update = fn(-msg, model) => {count: model.count + 1}
view = fn(model) => {
  kind: "Text",
  x: 100, y: 100,
  size: 48, color: 0xffffffff,
  content: "\${model.count}"
}
`;

// Load WASM and create TEA app
const wasm = await loadKitWasm('./kit-interpreter.wasm');
wasm.init();
const app = createTeaApp(wasm, kitSource);

// Render initial view
const canvas = document.getElementById('app');
const ctx = canvas.getContext('2d');
renderElement(ctx, app.getView());

// Handle events
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const view = app.mouseClicked(e.clientX - rect.left, e.clientY - rect.top);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  renderElement(ctx, view);
});

Note: Kit uses kind instead of type for element types (since type is a reserved keyword). The renderer accepts both.

API Reference

Runtime Functions

runTeaApp(options)

Run a TEA application with JavaScript model management.

runTeaApp({
  canvas: HTMLCanvasElement,
  init: () => Model,
  update: (msg: Msg, model: Model) => Model,
  view: (model: Model) => Element,
  config?: RuntimeConfig
})

createRuntime(canvas, config)

Create a low-level runtime for custom control.

const runtime = createRuntime(canvas, {
  targetFps: 60,
  backgroundColor: 0x1a1a2eff,
  autoClear: true,
  captureKeyboard: true,
  captureMouse: true,
  debug: false
});

runtime.start(initialView, updateFn);
runtime.stop();

Renderer

renderElement(ctx, element)

Render an Element tree to a Canvas 2D context.

const ctx = canvas.getContext('2d');
renderElement(ctx, {
  type: 'Rect-Fill',
  x: 10, y: 10, w: 100, h: 50,
  color: 0xff0000ff
});

intToColor(color)

Convert Kit RGBA integer to CSS color string.

intToColor(0xff0000ff) // => 'rgba(255,0,0,1)'

Events

setupEventCapture(canvas, onEvent, options)

Set up DOM event capture for a canvas element.

const cleanup = setupEventCapture(canvas, (msg) => {
  console.log(msg.kind, msg.x, msg.y);
}, {
  captureKeyboard: true,
  captureMouse: true,
  preventDefaults: true
});

// Later: cleanup();

Event Constructors

import {
  frameTick,      // (dt) => Msg
  keyPressed,     // (keyCode) => Msg
  keyDown,        // (keyCode) => Msg
  keyReleased,    // (keyCode) => Msg
  mouseClicked,   // (x, y, button) => Msg
  mouseMoved,     // (x, y) => Msg
  mouseWheel      // (delta) => Msg
} from 'kit-tea-web';

Element Types

All Kit TEA Element types are supported:

Shapes

  • Rect-Fill - Filled rectangle
  • Rect-Outline - Rectangle outline
  • Rect-Rounded - Rounded rectangle
  • Circle-Fill - Filled circle
  • Circle-Outline - Circle outline
  • Line - Line segment
  • Triangle - Filled triangle
  • Polygon - Filled polygon

Text

  • Text - Basic text
  • Text-Styled - Styled text with font options

Gradients

  • Rect-Gradient-H - Horizontal gradient
  • Rect-Gradient-V - Vertical gradient
  • Rect-Gradient-D - Diagonal gradient
  • Rect-Gradient-Radial - Radial gradient

Layout

  • Group - Group of elements
  • Row - Horizontal layout
  • Column - Vertical layout
  • Container - Positioned container
  • Scrollable - Scrollable region
  • Empty - Empty element
  • Spacer - Spacing element

Widgets

  • Button - Clickable button
  • Slider - Value slider
  • Progress-Bar - Progress indicator
  • Checkbox - Checkbox input
  • Radio - Radio button
  • Toggler - Toggle switch
  • Text-Input - Text input field
  • Pick-List - Dropdown select
  • Text-Editor - Multi-line text editor

Key Codes

Key codes match Raylib conventions. Common codes:

KeyCode
Up Arrow265
Down Arrow264
Left Arrow263
Right Arrow262
Space32
Enter257
Escape256
A-Z65-90
0-948-57

Color Format

Colors are 32-bit integers in RGBA format: 0xRRGGBBAA

const red = 0xff0000ff;      // Full red, full opacity
const semiBlue = 0x0000ff80; // Blue at 50% opacity
const white = 0xffffffff;

Touch & Gamepad Support

Touch events are automatically mapped to mouse events for seamless mobile support:

  • touchstart -> mouse-down
  • touchmove -> mouse-moved
  • touchend -> mouse-released + mouse-clicked

Gamepad support (opt-in):

runTeaApp({
  canvas,
  config: {
    captureGamepad: true,
    gamepadDeadzone: 0.15,
  },
  // ...
});

Gamepad events:

  • gamepad-button-pressed - button index in msg.button
  • gamepad-button-released - button index in msg.button
  • gamepad-axis - axis index in msg.button, value (-1 to 1) in msg.dt

WebGL Renderer

For maximum performance with many shapes, use the WebGL renderer:

import { createHybridRenderer } from 'kit-tea-web';

const canvas = document.getElementById('app');
const renderer = createHybridRenderer(canvas);

function frame() {
  const view = buildView();
  renderer.render(view, 0x1a1a2eff); // backgroundColor

  // Get performance stats
  const stats = renderer.getStats();
  console.log(`Triangles: ${stats.triangleCount}, Draw calls: ${stats.drawCalls}`);

  requestAnimationFrame(frame);
}

Renderers Available

RendererUse CaseText Support
renderElement (Canvas 2D)Simple UIs, full feature supportNative
createWebGLRendererMaximum shapes, no textNone
createHybridRendererMany shapes with textCanvas 2D fallback

Performance Tips

  • WebGL batches similar shapes into single draw calls
  • Use createHybridRenderer when you need both shapes and text
  • For 1000+ animated shapes, WebGL can be 5-10x faster

Optimized Rendering

For complex UIs, enable element diffing to minimize redraws:

runTeaApp({
  canvas,
  config: {
    optimizedRendering: true, // Enable diffing
    showDirtyRects: true,     // Debug: show redrawn regions in red
  },
  // ...
});

How it works:

  • Compares previous and current element trees
  • Calculates "dirty rectangles" for changed elements
  • Only redraws regions that changed
  • Merges overlapping dirty rects to reduce draw calls

Best for:

  • UIs with many static elements
  • Small, frequently-changing elements (cursors, animations)
  • Complex scenes where full redraws are expensive

Access optimization stats:

const stats = app.runtime.getStats();
console.log(stats.diffing.efficiency); // "85.5%"
console.log(stats.diffing.partialRedraws); // 342
console.log(stats.diffing.fullRedraws); // 12

Hot Module Replacement (HMR)

For development, use HMR to reload code while preserving model state:

import { createHmrApp, createHmrIndicator } from 'kit-tea-web';

const hmr = createHmrApp({
  preserveState: true,  // Keep model across reloads
  verbose: true,        // Log HMR events
  onReload: (count) => console.log(`Reload #${count}`),
});

// Initial load
hmr.load({ init, update, view }, runtimeConfig, canvas);

// Hot reload with new view function
hmr.load({ init, update, view: newViewFn }, runtimeConfig, canvas);

// State is automatically preserved!
console.log(hmr.getModel()); // { count: 5, ... }

// Add visual indicator
createHmrIndicator(hmr, { position: 'bottom-right' });

Automatic File Watching

For simple dev setups without a bundler:

import { setupDevMode } from 'kit-tea-web';

const dev = setupDevMode(canvas, './app.js', {
  watchInterval: 1000,
  runtimeConfig: { debug: true },
  onReload: () => console.log('Reloaded!'),
});

// Later: dev.stop()

HMR API

MethodDescription
hmr.load(app, config, canvas)Load/reload app
hmr.updateView(viewFn)Hot-swap just the view function
hmr.getModel()Get current model state
hmr.saveState()Manually save state
hmr.clearState()Clear saved state
hmr.getStats()Get reload statistics

Debug Overlay

Enable with config.debug: true:

runTeaApp({
  canvas,
  config: {
    debug: true,
    debugOptions: {
      showFps: true,        // FPS counter
      showFrameTime: true,  // Frame time in ms
      showEventCount: true, // Events per frame
      showMemory: true,     // JS heap usage (Chrome only)
      position: 'top-left', // 'top-left', 'top-right', 'bottom-left', 'bottom-right'
    },
  },
  // ...
});

HTML Widgets

For accessible form inputs, use native HTML elements overlaid on the canvas:

import { createWidgetManager } from 'kit-tea-web';

const widgets = createWidgetManager(canvas, {
  container: document.getElementById('canvas-container'), // Parent element
  zIndex: 10,
});

// Create widgets
widgets.createTextInput('name', {
  placeholder: 'Enter name',
  onChange: (value) => updateModel({ name: value }),
});

widgets.createSelect('color', {
  options: [
    { value: 'red', label: 'Red' },
    { value: 'blue', label: 'Blue' },
  ],
  onChange: (value) => updateModel({ color: value }),
});

widgets.createRange('size', {
  min: 10, max: 100, step: 5,
  onChange: (value) => updateModel({ size: parseInt(value) }),
});

widgets.createCheckbox('filled', {
  label: 'Filled',
  onChange: (checked) => updateModel({ filled: checked }),
});

widgets.createButton('submit', {
  label: 'Submit',
  onClick: () => handleSubmit(),
});

// Position widgets (call after canvas layout)
widgets.updatePosition('name', 100, 50, 200, 30);
widgets.updatePosition('color', 100, 100, 150, 30);

// Get/set values
widgets.setValue('name', 'Default');
const name = widgets.getValue('name');

// Show/hide
widgets.hide('name');
widgets.show('name');

// Cleanup
widgets.dispose();

Available Widget Types

MethodDescription
createTextInput(id, opts)Single-line text input
createTextarea(id, opts)Multi-line text area
createSelect(id, opts)Dropdown select (PickList)
createCheckbox(id, opts)Checkbox with label
createRange(id, opts)Slider/range input
createButton(id, opts)Clickable button
createProgress(id, opts)Progress bar indicator
createToggler(id, opts)Toggle switch (on/off)
createRadio(id, opts)Radio button group

Benefits

  • Accessibility: Native keyboard navigation, screen reader support
  • Mobile: Native touch handling, auto-zoom on focus
  • Familiar: Standard form behavior users expect
  • Styling: Can be styled with CSS

Examples

See the examples/ directory:

  • counter/ - Basic counter with buttons
  • widgets/ - Interactive widget showcase
  • animation/ - Physics and frame timing
  • pong/ - Classic Pong game
  • touch/ - Touch drawing and gamepad support
  • optimized/ - Compare standard vs optimized rendering
  • hmr/ - Hot Module Replacement demo
  • webgl/ - WebGL vs Canvas 2D performance comparison
  • html-widgets/ - Native HTML form elements on canvas

To run examples locally:

cd examples/counter
python3 -m http.server 8000
# Open http://localhost:8000

Browser Support

  • Chrome 80+
  • Firefox 75+
  • Safari 13.1+
  • Edge 80+

Requires ES modules support.

License

MIT