@dimacrossbow/target-zone-vue
v0.1.0
Published
Vue 3 components for @dimacrossbow/target-zone-core — declarative targets, layers, zones, shots, decorations.
Maintainers
Readme
@dimacrossbow/target-zone-vue
Declarative Vue 3 components for
@dimacrossbow/target-zone-core. Build shooting-sport targets — image, scoring
zones, shots, decoration layers — with reactive components instead of
imperative ViewModel wiring. PIXI.js does the rendering; Vue owns the state.
Install
npm install @dimacrossbow/target-zone-vue @dimacrossbow/target-zone-core pixi.js vue@dimacrossbow/target-zone-core, pixi.js (^8.6.6) and vue (^3.5.0) are peer
dependencies.
Quick start
<script setup lang="ts">
import { ref } from 'vue'
import { TargetCanvas, Target, Zone, Shot } from '@dimacrossbow/target-zone-vue'
import type { ITargetSurfacePoint, Zone as ZoneEntity } from '@dimacrossbow/target-zone-core'
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 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 } },
]
const shots = ref([{ id: 'a', x: 160, y: 160, color: '#ff3366' }])
let n = shots.value.length
function onSurfaceClick(point: ITargetSurfacePoint, zone: ZoneEntity | null) {
shots.value.push({ id: `s${n++}`, x: point.x, y: point.y, color: '#33aaff' })
if (zone) console.log('scored', zone.data.score)
}
</script>
<template>
<TargetCanvas :width="800" :height="600" :background="0x12141a">
<Target :width="320" :height="320" image="/target.png" @surface:click="onSurfaceClick">
<Zone v-for="z in zones" :key="z.name" v-bind="z" />
<Shot
v-for="s in shots"
:key="s.id"
:x="s.x"
:y="s.y"
:color="s.color"
image="/arrow.png"
mask="/arrow-mask.png"
/>
</Target>
</TargetCanvas>
</template>Everything is reactive: push to shots and the sprites appear; toggle
visible and layers hide. The components diff and mount/unmount the
underlying core ViewModels for you.
Components
<TargetCanvas>
Owns the PIXI Application and camera; resizes when its dimensions change.
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| width * | number | — | Canvas width in px. |
| height * | number | — | Canvas height in px. |
| background | number | 0x202020 | Background color 0xRRGGBB. |
<Target>
Builds a Target and mounts it into the nearest layer. Zones/shots can be
passed in bulk via :zones / :shots, or declared as child components.
| Prop | Type | Default |
| --- | --- | --- |
| width * / height * | number | — |
| image * | string | — |
| x / y | number | 0 |
| zones | IZoneData[] | [] |
| shots | IShotData[] | [] |
| autoBounds | boolean | true (auto object-fit: contain) |
Emits: surface:click (point, zone \| null, event), zone:click
(zone, event), shot:click (shot, event).
<Zone>
| Prop | Type | Default |
| --- | --- | --- |
| name * | string | — |
| geometry * | string | — (SVG path) |
| holes | string[] | [] (cut-out rings) |
| data | Record<string, unknown> | {} |
| highlight | IZoneHighlight | undefined |
| x / y | number | 0 |
| visible | boolean | true |
<Shot>
| Prop | Type | Default |
| --- | --- | --- |
| x * / y * | number | — |
| size | number | 40 |
| color | string | '#ffffff' |
| image / mask | string \| null | null |
| geometry | string \| null | null |
| anchorOffset | { x, y } | { x: 0, y: 0 } |
| data | Record<string, unknown> | {} |
| visible | boolean | true |
<TargetLayer> + asset components
Group decoration assets on a zIndex. Colors are numeric 0xRRGGBB.
<TargetLayer name="markers" :z-index="500">
<CircleAsset :x="160" :y="160" :radius="4" :color="0xffffff" />
<TextAsset :x="160" :y="285" text="Shots: 3" :style="{ fontSize: 14, fill: 0xffffff, fontWeight: 'bold' }" />
<PathAsset :geometry="hull" :fill="{ color: 0x33ff88, alpha: 0.2 }" :stroke="{ color: 0x33ff88, width: 2 }" />
<ImageAsset image="/logo.png" :x="300" :y="8" :width="48" :height="48" />
</TargetLayer><TargetLayer> props: name *, zIndex (default 1000), visible
(true), eventMode ('none' — set to 'passive'/'static' to receive
pointer events on its assets).
Pointer events
<Zone>, <Shot> and every asset component emit the PIXI pointer events:
@click, @pointer-over, @pointer-out, @pointer-down, @pointer-up,
@pointer-move. Events on layer-scoped assets require the enclosing
<TargetLayer :event-mode="'passive'"> (the default 'none' blocks the
subtree).
Custom asset components
For renderers not covered by the built-ins, compose your own with the exposed internals:
import { useLayerAsset, bindPointerEvents, POINTER_EVENT_NAMES } from '@dimacrossbow/target-zone-vue'
import { LayerKey, SceneKey, TargetKey } from '@dimacrossbow/target-zone-vue' // injection keysHistory / undo-redo
History is app-level — use core's generic History<ICommand> and mutate your
reactive shots from execute() / undo(). See the
vue-playground for a complete example.
License
MIT © dimacrossbow
