@dimacrossbow/target-zone-core
v0.1.0
Published
PIXI.js-based target visualization with MVVM split, layers, zones, shots, and decorations.
Maintainers
Readme
@dimacrossbow/target-zone-core
A flexible 2D target / scoring-zone renderer built on PIXI.js v8. The generic visual primitive behind shooting-sport apps (archery, biathlon, pneumatics, …): a configurable target image, arbitrary scoring zones, shots placed at hit coordinates, and a decoration slot for your own overlays.
Generic by design. The package renders and reports — it holds no domain logic. Scoring rules, data shapes and gameplay live in your app. Every
datafield is an opaqueRecord<string, unknown>.
Features
- 🎯 Configurable target — any backdrop image, sized in target-local space.
- ⭕ Arbitrary zones — circles, donuts, or custom SVG-path geometry with cut-out holes. Hit-testing respects the holes (a true ring, not a disc).
- 🔴 Shots — projectile sprites placed at hit coordinates, with per-shot color tinting via a colorable mask layer (or plain geometry).
- 👆 Input — tap / click on the target surface (mouse + touch), with the zone under the tap reported back to you.
- 🖼 Decoration layers — plug in MPI / group / annotation overlays through
addLayer()+ built-inCircleAsset/ImageAsset/TextAsset/PathAssetprimitives. - 🎥 Camera — pan, multi-touch pinch-zoom, wheel-zoom, and object-fit: contain auto-fit with clamps.
- ↩️ History — a generic
History<ICommand>undo/redo stack plus ready-madeAddShotCommand/RemoveShotCommand. - 💾 Serialization —
target.toJSON()↔Target.create(json)round-trip. - 🧩 PIXI-free domain subpath —
@dimacrossbow/target-zone-core/domainexposes the entity + event layer with zero PIXI runtime dependency, for non-PIXI renderers (see@dimacrossbow/target-zone-react-native).
Install
npm install @dimacrossbow/target-zone-core pixi.jspixi.js is a peer dependency (^8.6.6) — install it alongside.
Quick start
import {
SceneViewModel,
LayerViewModel,
Target,
TargetViewModel,
} from '@dimacrossbow/target-zone-core';
// 1. A scene owns the PIXI Application + camera. Mount it into a DOM element.
const scene = new SceneViewModel({
width: window.innerWidth,
height: window.innerHeight,
background: 0x12141a,
});
await scene.init(document.querySelector('#app')!);
window.addEventListener('resize', () =>
scene.resize(window.innerWidth, window.innerHeight),
);
// 2. A layer groups assets by zIndex.
const layer = new LayerViewModel({ name: 'targets', zIndex: 0 });
scene.addLayer(layer);
// 3. Build a target from plain data and add it to the layer.
const circle = (cx: number, cy: number, r: number) =>
`M ${cx},${cy} m -${r},0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${r * 2},0`;
const target = new TargetViewModel(
Target.create({
x: 0,
y: 0,
width: 320,
height: 320,
image: '/target.png',
zones: [
{ name: 'ring-9', geometry: circle(160, 160, 64), holes: [circle(160, 160, 38)], data: { score: 9 } },
{ name: 'bullseye', geometry: circle(160, 160, 38), data: { score: 10 } },
],
shots: [],
}),
);
layer.addAsset(target);
// Keep the target rect fitted on resize (object-fit: contain).
scene.setContentBounds({ x: target.x, y: target.y, width: target.width, height: target.height });
// 4. Register new shots on tap — you get the local point and the zone under it.
target.events.on('surface:click', ({ x, y, zone }) => {
target.addShot({ x, y, color: '#ff3366', image: '/arrow.png', mask: '/arrow-mask.png' });
if (zone) console.log('scored', zone.data.score);
});Concepts
MVVM entities
The library is split Model / View / ViewModel, with an Base → Asset
entity hierarchy:
- Model — pure state (
SceneModel,LayerModel,TargetModel). - View — PIXI display logic (
SceneView,TargetView,ZoneView, …). - ViewModel — orchestrates Model ↔ View, owns the event emitters and pointer
wiring (
SceneViewModel,TargetViewModel, …).
Compound components (Scene, Layer, Target) expose a flat ViewModel facade.
Leaf entities (Zone, Shot, Projectile, and the asset primitives) are
single classes that own their container and draw() directly.
Projectile vs Shot
A Projectile is a reusable template (image / mask / geometry /
anchorOffset). A Shot is an instance (x / y / size / color) that
references a projectile to render its visual. Many shots can share one
projectile.
Render stack
Inside target.container, bottom → top:
- Backdrop sprite (image)
- Zones (hover-highlight + donut-accurate hit-testing)
- Shots (tapping a shot stops propagation — it inspects, doesn't add)
- Decoration layers (your overlays)
Events
| Emitter | Event | Fires on |
| --- | --- | --- |
| target.events | surface:click | Tap on empty surface or a zone — payload { x, y, zone \| null } |
| target.events | zone:click | Tap on a zone (also bubbles → surface:click) |
| target.events | shot:click | Tap on a shot (stops propagation — no surface:click) |
| target.events | shot:added / shot:removed | Shot list mutations |
| scene.events | layer:added, camera:changed | Scene / camera changes |
History (undo/redo)
import { History, AddShotCommand } from '@dimacrossbow/target-zone-core';
const history = new History({ capacity: 100 });
history.execute(new AddShotCommand(target, { x, y, color: '#ff3366' }));
history.undo();
history.redo();
history.events.on('changed', () => update(history.canUndo, history.canRedo));ICommand is generic — implement your own commands for any app-level mutation.
Public API
Import everything from the package root — internal file paths are not part of the contract:
- Entities —
Base,Asset,CircleAsset,ImageAsset,TextAsset,PathAsset,Projectile,Shot,Zone,Target - Models —
SceneModel,LayerModel,TargetModel - Views —
SceneView,LayerView,TargetView,ZoneView,ShotView, and the asset views - ViewModels —
SceneViewModel,LayerViewModel,TargetViewModel,ZoneViewModel,ShotViewModel, and the asset view-models - History —
History,AddShotCommand,RemoveShotCommand - Utilities —
EventEmitter,Units,UnknownUnitError
All types (ITargetData, IShotOptions, IZoneJSON, TargetEventMap, …) are
exported alongside their classes.
@dimacrossbow/target-zone-core/domain
A PIXI-free subset — entities, models, EventEmitter, Units — with zero
PIXI runtime dependency. Use it in environments where PIXI can't run (native,
server-side data building). A few .d.ts files carry type-only PIXI references
that are erased from the emitted JS, so pixi.js is only needed for type
resolution.
Conventions
- Colors are numbers (
0xRRGGBB) everywhere exceptShot.color, which is astring(PIXI accepts both). - Coordinates are target-local (
0..width × 0..height). Projectile geometry (when used instead of an image) is normalized0..1and scaled byShot.size. datafields are always opaqueRecord<string, unknown>.- Decorations are not serialized (runtime/consumer-derived state).
Related packages
| Package | For |
| --- | --- |
| @dimacrossbow/target-zone-vue | Declarative Vue 3 components |
| @dimacrossbow/target-zone-react-native | Expo / RN (Skia) renderer |
License
MIT © dimacrossbow
