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

@glowbox/neon

v1.12.0

Published

Glass-tube neon sign component — text bent into single-stroke tubes (vendored Hershey faces), visible unlit glass, electrode strike sequences, ageing tubes, flasher programs and an optional transformer hum, in the glowbox family.

Downloads

790

Readme

@glowbox/neon

A glass-tube neon sign component — a sibling rendering core to @glowbox/nixie, @glowbox/led-grid, @glowbox/seven-segment, @glowbox/flip-dot and @glowbox/split-flap. CSS text-shadow puts a glow on any font for free; this gives you the glass: tubes bent from single-stroke letterforms that exist as objects — visible unlit, struck alive through arcing electrodes, ageing until a section dies — with an optional transformer hum. Zero runtime deps.

yarn add @glowbox/neon
import { createNeonSign } from '@glowbox/neon';

const sign = createNeonSign(canvas, { text: 'Cocktails', gas: 'rose', sound: true });
sign?.power(false); // dark glass on the wall
sign?.power(true); // …and the strike sequence

Give it a canvas; it owns the 2D render, the strike/flicker animation, resize, and the render loop — which runs only while something is in flight; a resting sign, lit or dark, costs nothing.

What a text-shadow can't do

  • Real tube geometry. Letterforms are stroked centrelines — the path the glass bender follows — from vendored Hershey single-stroke faces, corner- rounded into bends, one constant-width tube per section with an electrode stub at each free end. Categorically different from glowing filled Helvetica.
  • The unlit tube is drawn, always. Powered off or dead, the glass still hangs on the wall: phosphor-coated fills show their paint, clear gases pale glass. A text-shadow at 0 opacity is nothing.
  • A strike is a sequence, not a fade-in. The electrodes arc while the tube stays dark, ignition takes with a few partial pops, overshoots white-hot and settles; sections stagger like independently-warming tubes ('reveal' walks them in order). Turn-off is near-instant — the discharge just stops.
  • The wear arc. age applies deterministic per-tube dimming; past ~0.7 the most-worn section starts flickering, from ~0.95 it is dead glass while the runner-up takes over — the MOT L sign, at tube granularity.
  • The transformer. sound: true hums at twice the mains frequency (level tracking how much tube is lit — it dies with a dropout) with a crackle as ignition takes. tired: true is the failing transformer: hard whole-sign dropouts, staggered re-strikes.

Letterforms

Two faces are vendored as packed single-stroke data (a few kB total): 'script' (Hershey Script — connected cursive, the classic window sign; one tube runs a whole word) and 'sans' (Hershey Simplex — block channel letters, one tube per glyph). Coverage is printable ASCII; anything else is skipped with one dev warning. Emoji/CJK are out of scope.

The font option also takes a custom NeonFont — a glyph table of centreline polylines (y-down, baseline 0) with capHeight/ascent/descent. A one-glyph face whose strokes are your logo works.

The Hershey Fonts were originally created by Dr. A. V. Hershey while working at the U. S. National Bureau of Standards; the format of the font data was originally created by James Hurt (Cognition, Inc.). The acknowledgement ships inside the package as the HERSHEY_LICENSE export (referenced by the face data, so bundlers keep it) and as LICENSE-hershey; full terms there.

Artwork

Real signs put the martini glass beside the word, not in it. art composes single-stroke pieces against the text block:

createNeonSign(canvas, {
	text: 'dice',
	gas: 'co2',
	art: [
		{ d: DIE_LEFT, place: 'left', rotate: -12, color: '#ff3355' },
		{ d: DIE_RIGHT, place: 'right', rotate: 9, color: '#7cd5ff' }
	]
});

d is SVG path data (a d string or several — author in any vector editor) or centreline polylines. place anchors the piece behind the text or beside it (left/right/above/below); size scales it against the text block's height, dx/dy nudge, rotate tilts. Each piece is its own tube — its own gas/color, its own strike, wear and death; tubes: 'path' splits it per subpath, steady: true wires it past the flasher cam (the diner border that stays lit around the blinking word), and behind pieces light first under 'reveal'.

Several pieces can be cut from one drawing: give them the same frame: [width, height] (the drawing's design space) and identical placement, and they keep their registration — the multi-colour portrait pattern, where the hair, the face and the jacket are separate gases in one artwork.

Pieces can overlap: mark the front one opaque: true and its closed subpaths become a solid face that cuts the tubes of everything behind it in z-order, ending each rear run shy of the front edge — glass can't hide glass, so the sign maker ends the run. Cut ends read as the tube diving behind (no electrode stub); a fully covered tube disappears. That's the classic overlapping dice pair: rear die first in the list, front die opaque.

Author centrelines: a filled icon's outline strokes as a double-walled silhouette. Put the bends in the path itself — text corners are auto-rounded, art is not. pathToStrokes(d) is exported on its own (pure, node-safe) for custom-font glyphs and 3D consumers.

Gas & colour

gas picks what's in the glass — it sets the lit colour, how white-hot the core runs, and what the dead glass looks like: 'neon' (the red-orange the medium is named for), 'argon' (pale blue), 'helium', 'co2', and the phosphor-coated 'green' / 'gold' / 'rose' (visibly painted even off). color overrides the lit colour — a single colour or one per text line (the NO / VACANCY pattern) — while the gas keeps shaping the core and the unlit glass. Patch color: null to go back to the gas. Under polarity: 'absorb' a gas discharges its own ink and a colour you name is the pigment — see below.

The light-theme answer: an element that shines dark

A bloom cannot read against white — which normally means neon signs only live on dark walls. So the core ships one invented element: polarity: 'absorb'. Its discharge runs dark: the same graduated ramp, multiplied into the wall instead of added to it, so the tubes ink a pale surface — halation, hot core, arcing electrodes and all, just inverted.

createNeonSign(canvas, { text: 'dice', polarity: 'absorb' }); // wall defaults pale

Colours still come from gas and color, under two rules. Each gas carries its own ink — that is where the invention lives, and it is why a co2 white tube inverts into a literal black light. A colour you name is the pigment itself, so its lightness means the same thing inking as it does lit: a pale tube lays down a faint mark, a saturated one a deep one, and two tubes of different lightness stay two tubes. That is what a gauge needs — a quiet track ring under a bright meter arc reads as one instrument in both themes, with one set of colours.

The hot core is a second coat of the same pigment rather than a march to neutral black, so a green tube inks green at its very centre; and ink bleeds tighter than light blooms, because a pigment sits in the surface while light scatters on its way through the air. An unnamed wall follows the polarity (near-black when emitting, near-white when absorbing); wall: null under absorb assumes a pale backdrop, since a multiply with nothing behind it treats the missing surface as white. The non-luminous parts key off the wall rather than the polarity, because glass and metal are just objects on a surface and what they need is contrast with it: glass (the unlit tube) and electrode (the metal end caps) default light on a dark wall and dark on a pale one — near-black specks on white read as dirt, not hardware — and both take your own colour when you want one. Pair it with your app's theme and the sign belongs in both.

Polarity is settable per art piece too, which is usually what a mixed sign wants: white dice can shine black on a pale card while the gold lettering beside them still shines gold — it's the element that runs dark, not the circuit.

Programs — the flasher cam

program is sign hardware, not content: 'steady', 'flash' (the whole sign), 'chase' (a dark slot running the sections), 'reveal' (sequential strike on power-on/setText). speed multiplies the cam rate, but every program is hard-capped below ~3 events/s — a photosensitivity guard the speed knob cannot defeat — and prefers-reduced-motion degrades every program to steady, snaps strikes, and disables flicker/tired.

Options

| option | default | notes | | --------------- | ------------- | ----------------------------------------------------------------------------------- | | text | '' | '\n' splits lines | | font | 'script' | 'script' | 'sans' | custom NeonFont | | art | — | single-stroke pieces behind/beside the text (see Artwork) | | gas | 'neon' | what's in the glass (colour, ink, hot core, dead-glass tint) | | color | — | override: one colour or one per line; the ink when absorbing | | wall | per polarity | behind the sign; null = transparent canvas | | polarity | 'emit' | 'absorb' = the invented dark discharge; a named colour is the ink | | on | true | off is not blank — the unlit glass stays visible | | lineOn | all on | per-line circuits | | wordOn | all on | per-word circuits: HOTEL 0, NO 1, VACANCY 2 — the NO cuts out on its own | | wordColor | — | per-word tube colour (null inherits the line/gas): the red NO on blue VACANCY | | glow | 0.7 | halation strength | | face | — | painted letter slabs under the tubes, lit by their own circuit; one per line | | outline | false | bend the tube around the letterform (the HOTEL border); one flag or per line | | lineScale | all 1 | per-line glyph scale (the headline HOTEL at twice the NO VACANCY); tube width holds | | glass | vs the wall | the unlit tube (the gas still tints it) | | electrode | vs the wall | the metal end caps: near-black on a dark wall, mid-grey on a pale one | | age | 0 | wear 0..1: dimming → flickering tube → dead glass | | flicker | 0 | electrical instability: sparse scheduled dips | | tired | false | failing transformer: whole-sign dropouts + re-strikes | | program | 'steady' | the flasher cam (rate-capped) | | speed | 1 | cam rate multiplier (the cap always wins) | | tubes | 'auto' | circuit granularity: per word; or glyph (channel letters) / word / line | | align | 'center' | per-line alignment | | lineSpacing | 1.1 | baseline advance × the face's ascent+descent | | letterSpacing | 0 | extra tracking (fraction of cap height); breaks script joins | | tilt | 0 | text block tilt, degrees (negative rises left-to-right) | | padding | 0.08 | canvas margin fraction | | strikeMs | 900 | one tube's strike (0 = instant; forced by reduced motion) | | sound | off | true (= 0.5) or 0..1 — the hum that follows the glass | | mains | 50 | 50/60 Hz — the hum's fundamental is twice this | | theme | 'dark' | 'light' / 'auto' bundle the colour defaults (see Themes) | | pixelRatio | 2 | cap on devicePixelRatio | | label | 'neon sign' | aria-label; the shown text is appended; '' hides |

All options update live via setOptions(patch). API: setText(text) (a change re-glasses and strikes on), power(on) (the wall switch), jolt(section?) + sectionAt(clientX, clientY) (see Tappable tubes), resize(), snapshot() (PNG data URL), dispose() (hands the canvas back clean). A sign is created lit — no boot animation; the strike show is opt-in via power() cycling, setText, or program: 'reveal'.

Tube sections — one tube per circuit

A section is the unit that strikes, flickers, ages and dies together — one circuit, one bent tube, one electrode pair, and the pair always sits on the routed run's two free ends (where the bender starts and finishes, never wherever the font data happened to). Every stroke end in between is bare glass diving behind: the joints link through crossover runs bent back off the face plane and painted out — real but invisible, exactly the part of a sign you never see.

tubes picks how much glass shares one circuit. 'auto' (the default) wires per word — how a real sign is bent, a circuit of one glyph being no circuit; script words were always one bent tube. tubes: 'glyph' cuts to channel-letter circuits — per-letter tubes with their own pairs, per-letter strikes and per-letter wear (the classic dying-letter MOT L needs this, since a wired word lights and dies as one tube) — and tubes: 'line' gives one circuit per text line (per-line colours pair well).

The switch panel matches: lineOn cuts whole lines, and wordOn/wordColor address WORDS, counted across the text in reading order — the motel sign proper, where the red NO (wordColor: [null, '#ff3b30', null]) strikes in and out on its own circuit (wordOn: [true, full, true]) while the blue VACANCY beside it never goes out. Cut glass stays on the wall; switching a circuit back on strikes it. lineScale sizes the typography per line — the headline HOTEL at twice the NO VACANCY under it — while the tube keeps its regular width whatever the letter size.

Painted faces & outlined letters

face is the movie-motel pattern: each letterform painted on the panel as a wide slab, an object that catches the tube's light. The paint shows unlit (keyed against the wall, like the glass and the hardware) and is lit by its own circuit — the wash follows the section's level and sits brightest beside the glass, falling back to plain paint away from it — so strikes, flicker and the wear arc play across the letter itself, not just the tube. One colour or one per text line (null skips a line: face: ['#e8e4da', null] paints HOTEL and leaves NO VACANCY bare), and an art piece takes its own face. Paint never reaches the tube's brightness — a lit face is washed paint, not a second light source — and an absorbing tube gives no light to catch, so its face stays paint.

outline completes the pattern: the tube is bent around the letterform instead of along it — a normal-width tube tracing the painted letter's border, the counters included, the way the classic HOTEL signs are built. The face paint keeps the letterform itself; the tube, its electrodes, strikes and taps all live on the contour (an O really is two tubes, the outer ring and the counter). One flag or one per line, like face. Outlined letters are wider than their skeletons, so pair with a little letterSpacing; art corners are not rounded and offset poorly, so outline is at its best on text.

The sound engine

The sign's voice is its electricity. The transformer hum's level follows the lit glass, so a staggered strike flutters it in from silence as the tubes pop through their ignition flickers, wear dips duck it, and a dropout kills it dead — the only event sound is a subliminal solenoid tick as each tube's starter engages and its ignition takes (flavour, not foley). The voice is createHum({ volume, base }), exported on its own: a continuous synth over the shared, refcounted AudioContext (the mains-doubled fundamental plus detuned harmonics and a whisper of low-passed sizzle), driven by setLevel(0..1). Held at 0 it tears its sources down; hidden tabs mute it. createMechSound (the mechanical cores' tick synth) rides along in the same vendored engine. Sound starts on the first user gesture — nothing to wire up.

Tappable tubes

The sign attaches no pointer handlers — it's a display. It answers the geometry question instead, and owns what a disturbed tube does; you own when that happens:

canvas.addEventListener('pointerdown', (e) => {
	const tube = sign.sectionAt(e.clientX, e.clientY); // null if the tap missed
	if (tube != null) sign.jolt(tube); // that tube stutters
});

jolt(section?) is a physical event on the model, not a canned effect: the core supplies the dip, the eased recovery, the stutter back through the ignition pops, the hum ducking with it, and the reduced-motion policy — the same behaviour flicker schedules on its own. Omit the index for a whole-sign shudder; dark and dead glass ignore it. Effects stay yours: "flicker the die that lost", "pulse twice on a six" is app logic composed from jolt, power and setText.

Accessibility follows split-flap's split: the canvas is an image (role="img"), never a widget. If tapping does something meaningful in your app, the semantics are yours — put a real control over or beside the sign.

For 3D consumers

The text→tube pipeline is pure and exported: layoutTubes(text, font, opts) returns the corner-rounded centreline polylines + electrode ends per section in sign units (y-down, baseline 0) — extrude them into real tube geometry on your side, the way nixie's nixieCathodes tells the same story. Runs in bare node.

Performance

Path2D per section, built lazily and cached until the text/font changes; passes are stroked with shadowBlur capped and an LOD that drops halation and electrodes below ~28 px cap height. A sign is typically 5–30 sections — the render loop stops the moment the last transient settles.

Themes

theme bundles the two options that decide how a sign lives on a page: 'dark' (the default — lit tubes on a near-black wall), 'light' (polarity: 'absorb' on a near-white wall, because a bloom cannot read against white), or 'auto' to follow the page's prefers-color-scheme. Set polarity or wall yourself and that half is yours from then on.


Framework wrappers ship <NeonSign> alongside the other cores: @glowbox/svelte · @glowbox/react · @glowbox/vue. Pairs with @glowbox/crt. Live demo: https://eetu.github.io/glowbox/neon — turn the sound on, and let the Cocktails show power-cycle once.