Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Canvas Pointer Laravel Package

ruelluna/canvas-pointer

A Laravel package that adds an interactive canvas pointer field to forms, letting users click an image to select and store X/Y coordinates. Ideal for image hotspots, annotations, and coordinate pickers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require ruelluna/canvas-pointer
php artisan vendor:publish --tag="canvas-pointer-assets"  # if the package ships compiled assets
php artisan filament:assets                                # Filament's own asset hook, if used

Unlike a full Filament plugin, this is a form field, so there is no ->plugin() registration step. You import the field class directly into the resource/form schema. Check composer.json for the supported Filament major version before installing — a field built for v3 will not load cleanly in a v4 panel without adjustments.

Where to look first

  1. The field class under src/ — this is the fluent API you actually call (->image(), ->disk(), marker limits, etc.).
  2. The Blade view — tells you how the image and canvas are rendered and which Alpine/Livewire properties hold the marker state.
  3. The JS entrypoint — where click coordinates are computed and written back to the Livewire component. This is where coordinate-system bugs will live.

Read those three before writing any code; the whole package is small enough to skim in ten minutes, and the README will not explain the state shape.

First use case

The canonical fit is "click a location on a static image and store it": annotating a floor plan, marking defects on a photo, placing pins on a scanned map, cropping-adjacent tagging. Start with a single image + multiple markers, stored on a JSON column:

use Ruelluna\CanvasPointer\Forms\Components\CanvasPointer;

CanvasPointer::make('markers')
    ->image(Storage::disk('public')->url($record->photo_path))
    ->label('Damage locations')
    ->required(),

On the model:

protected $casts = [
    'markers' => 'array',
];

Verify the exact method names in the field class before committing to them — the intent above is the standard Filament convention, but the source is authoritative. Render the form once, click a marker, and dd($this->data['markers']) in the Livewire component to learn the array shape the package emits. Do that before designing your schema around it.

Implementation Patterns

Single marker vs. many

Most real uses are a single point with a label ("primary entrance"). Keep two distinct field types in your schema rather than overloading one: a single-marker field for "pick a point" and a multi-marker field for "annotate everything." If the package only exposes one mode, enforce the difference in validation:

->rules([
    fn (): Closure => function (string $attribute, $value, Closure $fail) {
        if (is_array($value) && count($value) > 1) {
            $fail('Only one point may be selected.');
        }
    },
])

Storing images correctly

The field takes an image URL, not an upload. In practice you pair it with a FileUpload field on the same form and feed the URL in reactively:

FileUpload::make('photo_path')
    ->image()
    ->disk('public')
    ->live()
    ->afterStateUpdated(fn (Set $set) => $set('markers', [])),

CanvasPointer::make('markers')
    ->image(fn (Get $get) => $get('photo_path')
        ? Storage::disk('public')->url($get('photo_path'))
        : null),

Two things matter here: resolve the URL through the same disk the field expects (a storage/ path will 403 or 404), and clear markers when the image changes, otherwise old coordinates point at a different picture. That reset is not automatic.

Normalize coordinates on save

Persist coordinates as percentages of the image dimensions, not raw pixels. Pixels tie your data to one rendering size and break the moment the field is shown wider or on mobile. If the package emits pixels, convert in a dehydrateStateUsing() or a model mutator:

->dehydrateStateUsing(fn (?array $state, Get $get) => collect($state)
    ->map(fn ($m) => [
        'x' => round($m['x'] / $get('image_width') * 100, 4),
        'y' => round($m['y'] / $get('image_height') * 100, 4),
        'label' => $m['label'] ?? null,
    ])->all())

Reversing this on formatStateUsing() for display keeps the round-trip symmetric. Store image_width/image_height alongside the file if you want to convert back reliably.

Read-only display

For view pages and tables, don't reach for the field — reuse the same percentage data in a small Blade partial that absolutely-positions dots over the image. This keeps the interactive field out of read-only contexts and makes the markers printable and exportable. A table column can render it with ->html():

Tables\Columns\TextColumn::make('markers')
    ->html()
    ->formatStateUsing(fn ($state) => view('partials.marker-pins', [
        'markers' => $state,
        'image' => $record->photo_path,
    ])->render()),

Validating before save

Markers are optional structure by default. Add explicit rules for the business requirement: minimum count, presence of a label, or "at least one marker inside a region." Because the state is a plain array, standard array validation works:

->rules(['array', 'min:1', 'max:10'])
->validationMessages(['min' => 'Drop at least one pin on the image.'])

Gotchas and Tips

Coordinate drift is the number-one failure mode. Click positions are computed relative to the rendered element, so they go wrong when: the image is CSS-scaled (max-width, object-fit), the browser zoom is not 100%, or the container resizes after the JS binds. If your stored coordinates look "close but shifted," you're capturing offset/clientX instead of scaled position, or dividing by the natural image size while the element is rendered at a different one. Test at a narrow viewport and at 125% zoom before shipping.

Livewire re-renders can wipe canvas state. Any reactive update on the form (->live() on a sibling field) can re-render the field's DOM and reset the canvas layer even though the PHP state is intact. Keep marker state in Livewire (so it survives) and make sure the JS rehydrates from the state on morph, not just on first mount. If markers vanish on unrelated keystrokes, this is why.

Asset pipeline. Check whether the package ships a prebuilt JS bundle or expects Vite. If it references resources/js directly, you'll need a build step in your app or a published asset path. Symptom: the image renders but clicks do nothing, with a 404 for the script in the console.

Filament version pairing. The field API differs between Filament v3 and v4 (Forms\Components namespaces, Set/Get vs. older $set/$get closures, action modal behaviour). The last release date suggests v3-era code; confirm the constraint in composer.json rather than guessing, and pin the version in your composer.json if you're on a mixed-version stack.

JSON column nulls. An empty marker set may come back as [], null, or a JSON string depending on the field's dehydrate behaviour and your cast. Normalize with ?? [] when reading and be explicit that array casts on a nullable column will surprise you in whereJsonContains queries. If you need to query by coordinates, store derived values in plain columns.

Debugging loop. Fastest path: browser console → inspect the Livewire component's wire:model property after a click; then ray() or logger() inside dehydrateStateUsing(); then the network payload of the updateProperty request. That isolates whether the bug is in the click handler, the state binding, or the persistence layer.

Extension points. The Blade view is publishable — if the default marker UI is too bare (no delete handle, no drag, no labels), publish and override the view rather than fighting the field with CSS. For dragging, listen for the existing click event and layer your own handlers on the marker elements; the package owns the image canvas, not your annotations.

Don't use this for editable vector data. If you need draggable polygons, zoom/pan, or freehand drawing, this field will run out of road quickly — reach for a dedicated canvas library in a custom Livewire component instead. This package's value is that it's one line in a Filament schema, and that value disappears the moment you need real geometry.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib