Skip to main content

Documentation

No results found.
Features

Animated Backgrounds

WebProCMS supports three ways of giving any page section a distinctive background: built-in static texture overlays, editor-uploaded looping videos, and animated <canvas> presets. Only the latter two actually move — the texture tier i...

WebProCMS supports three ways of giving any page section a distinctive background: built-in static texture overlays, editor-uploaded looping videos, and animated <canvas> presets. Only the latter two actually move — the texture tier is a static CSS/SVG pattern layer, the free always-on default. All three are applied through the same row paintbrush ("Design") panel on any section in any row — no new row template required.


When to use it

A motion background is a strong but expensive design choice. Use it for:

  • Hero sections that need to communicate energy or activity (SaaS launches, product reveals, conference pages, agencies)
  • Brand statements where a static color or static image would feel flat
  • Backgrounds-only loops — short, atmospheric video that adds depth without competing for the visitor's attention

Don't use it for:

  • Content-dense rows where motion competes with the message
  • Sections containing dense text the visitor needs to read carefully
  • Visitors on flaky / metered connections (heavy hero videos eat data); the static texture overlays have no bandwidth cost and are a safer default

Texture overlays are static — there's no motion to reduce, so they render identically regardless of the visitor's OS motion preference. The animated canvas presets (v3 below) do respect prefers-reduced-motion: the first frame paints, then the loop stops. Video backgrounds autoplay muted with playsinline so they don't block scroll on mobile (video isn't gated by prefers-reduced-motion).

What's included

There are three tiers, all selected from the row paintbrush panel:

v1: Static texture overlays (no upload required, on by default)

Twenty-two named patterns ship with the CMS as a static -z-10 decoration layer painted behind the section content — no motion, no brand colour tokens, no per-row colour configuration. Selected via Section::textureOptions() in app/Support/DlSchemas/Section.php:

grid, graph-paper, dots, diagonal-lines, crosshatch, isometric, diagonal-hatch, blueprint, plus-marks, concentric, hex-mesh, triangle-mesh, brick, herringbone, chevron, fish-scale, quatrefoil, topographic, circuit, sparkles, vignette, mosaic, pixel-mosaic, pixel-mosaic-full.

Two rendering mechanisms, chosen per texture in section.blade.php's $textureDecorationHtml match block:

  • Tailwind arbitrary-value gradients — grid, graph-paper, dots, diagonal-lines, and crosshatch are bg-[linear-gradient(...)] / bg-[radial-gradient(...)] utilities with an explicit dark: colour-pair variant (--color-tone-* swapped for a darker shade); isometric, diagonal-hatch, and concentric use a dual black/white rgba() emboss (repeating-linear-gradient / repeating-radial-gradient) so they read on light and dark backgrounds without a separate dark: variant; vignette is a single radial rgba(0,0,0,…) darkening fade.
  • Inline SVG patterns — the rest (blueprint, plus-marks, hex-mesh, triangle-mesh, brick, herringbone, chevron, fish-scale, quatrefoil, topographic, circuit, sparkles, mosaic, pixel-mosaic, pixel-mosaic-full) are built by dedicated Section::{name}Svg() PHP methods that emit a tiling <pattern> — several of them (pixel-mosaic, mosaic, hex-mesh, topographic, …) use a fixed RNG seed (mt_srand(N)) so the generated markup is byte-stable across renders.

Two companion fields tune every texture:

  • section_texture_opacity — fades the tuned baked strength toward invisible. '' or 100 renders at full (tuned) strength; the value multiplies the whole decoration's CSS opacity.
  • section_texture_fade — a fade-direction enum (Center default / Edges / Top / Bottom / Even — no fade) resolved by Section::textureMaskCss() into a CSS mask-image. Only applies to the line/grid-style textures listed in Section::textureCenterMaskStops(); textures with their own intrinsic masking (vignette, pixel-mosaic, pixel-mosaic-full) ignore it.

All textures:

  • Are static — no @keyframes, no JavaScript runtime, nothing to pause or reduced-motion-gate
  • Cost only the inline CSS/SVG emitted for that one section; no per-page asset, no extra HTTP request
  • Are neutral (black/white/tone-based), not brand-coloured — unlike the video and canvas tiers, texture doesn't reference primary/secondary CSS variables
  • Can also be set as a section preset default (preset['texture']) so every row using that preset inherits the same look unless a row overrides section_texture itself

v2: Editor-uploaded looping video backgrounds

Editors can upload an MP4 / WebM / MOV video to the media library and apply it as a section background. The system handles:

  • Video uploads in the media library. The library accepts video files up to 100 MB; videos appear in the grid with a play-icon overlay and render their first frame as a thumbnail via preload="metadata" (no full download). Existing image-only behaviour is preserved for every other media surface.
  • A picker filter (kindFilter). Pickers can be opened in video-only mode, image-only mode, or "show all" mode. The bg-video field uses video-only. Existing pickers (post hero, gallery, blog featured, etc.) keep their image-only behaviour with zero migration.
  • The poster belongs to the video's media-library item, not the section. There is no per-section poster field. At upload time the browser extracts the first frame to a JPEG (canvas-based, ~250ms, no server roundtrip, no ffmpeg) and the server stores it as a role=poster MediaItem linked to the video via media_items.poster_media_id (hidden from browse grids). Any video that lacks a poster — uploaded elsewhere, or whose poster was removed — can have one generated on demand from its media-library detail panel (Regenerate from video / Upload / Remove). Extraction is best-effort: if the browser can't decode the format the video still uploads cleanly and the poster can be added later.
  • Rendering. The video renders as <video autoplay muted loop playsinline preload="metadata"> positioned absolutely behind section content (-z-20). The poster= attribute is resolved at render from the video's linked poster (MediaItem::posterUrlForPath(), query-free + cache-busted) — no per-placement poster field. An optional darkening overlay improves text legibility.

The video plays from the public storage URL directly — no Glide processing, no streaming server. Browsers handle progressive download natively.

v3: Canvas-based animated backgrounds

Eleven built-in presets that paint into a <canvas> behind the section content. Each preset reads brand colour CSS variables (--color-primary, --color-secondary, --color-tone-500) at init time, so the look automatically matches the install's branding — and every preset also exposes its own tweakable settings (see below) for editors who want to push past the defaults.

Canvas 2D presets:

  • Particle Field — small bright points drifting in a chosen direction (up / down / left / right), wrapping seamlessly. Subtle enough to sit behind dense content. Fades out toward the bottom of the section by default.
  • Gradient Orbs — 2–6 concentrated bright blurred spheres moving with parallax-style depth offsets.
  • Lattice Waves — geometric grid that ripples in 3D-style waves, with sparse highlight dots at intersections. Modern / technical feel.
  • Color Sweep — single smooth multi-stop linear gradient sweeping across the section at a configurable angle. Minimal motion, low CPU, good when you want subtle.
  • Constellation — drifting dots connected by faint lines whose opacity scales with proximity. Reads as tech / connectivity / network. Good for SaaS, infrastructure, security pages.
  • Halftone Shift — grid of brand-coloured dots whose radii pulse with a travelling wave. Print-style halftone pattern with slow visible motion. Editorial / design-y vibe — best paired with a subtle dark overlay if foreground text density is high.
  • Bokeh Drift — large soft out-of-focus light discs drifting slowly with a gentle breathing pulse. Photographic depth-of-field look. Warm / atmospheric, good for portfolios + brand pages.
  • Fireflies — small glowing points wandering on organic paths, each blinking on its own rhythm. Warm and calm; suited to dark or nature-themed sections.
  • Wave Ribbons — layered sine ribbons flowing horizontally, weaving in and out of each other. A handful of strokes per frame — very low CPU.

WebGL presets (GPU fragment shaders; each falls back to a Canvas 2D preset when WebGL2 is unavailable or compilation fails):

  • Night Sky — the view from the ground on a spinning earth: three star layers plus a hazy Milky Way band (fbm clouds with dark dust lanes) rotate rigidly around a celestial pole, so stars trace slow arcs whose local direction varies across the section — nothing scrolls in a single straight line. Occasional shooting stars streak across. Deep-night background subtly tinted by the brand primary at the horizon. Falls back to Particle Field.
  • Aurora Veil — northern-lights curtains hanging from an undulating ridge near the top, with shimmering vertical rays through two depth layers. Color blends primary → secondary down each curtain; transparent background so it composites over the section's own bg color. Falls back to Gradient Orbs.

Tweakable parameters. Every canvas preset exposes per-section controls in the row paintbrush, under the canvas accordion — sliders (density, size, speed, intensity, …) and dropdowns (color family, drift direction, rotation center, …), plus a shared Edge fade select that masks the canvas out toward the top, bottom, or both edges. The param schema lives in Section::canvasPresetParams() and is consumed by both the editor UI and the runtime. Values are stored as JSON in content_overrides.section_animated_canvas_params and emitted on the canvas as data-anim-params="{...}". WebGL presets receive numeric values as u_{key} uniforms; Canvas 2D presets receive the sanitized object as state.params; the fade key is consumed server-side as a CSS mask on the canvas element. Defaults declared in PHP match the fallbacks inside each preset, so "no params saved" and "all controls at default" render identically.

All presets:

  • Are lazy-loaded — the runtime (resources/js/animated-bg/) only loads when there's at least one <canvas data-anim-preset> on the page, and Vite emits one async chunk per preset so only mounted presets download. Pages without a canvas pay zero bytes.
  • Auto-pause via IntersectionObserver when the section scrolls off-screen, so CPU isn't burning on a hero the visitor can no longer see
  • Honour prefers-reduced-motion — the first frame paints, then the loop stops
  • DPI-aware — canvas internal resolution = displayed pixels × devicePixelRatio (capped at 2 to avoid burning CPU on Retina 5K displays)
  • Resize-aware via ResizeObserver — section size changes (responsive breakpoints, content reflow) re-paint without reload

WebGL specifics:

  • Fullscreen-triangle vertex shader (no VBO needed — positions come from gl_VertexID) and a per-preset fragment shader
  • getContext('webgl2') is requested with powerPreference: 'low-power' so laptops on battery don't switch to the discrete GPU
  • WebGL context loss (webglcontextlost / webglcontextrestored) rebuilds the program + viewport on restore

Higher CPU than the static texture tier. Each canvas runs its own RAF loop costing ~1-3% sustained CPU on a modern laptop (the WebGL presets do their work per-pixel on the GPU, keeping the main thread mostly free). Reserve canvas presets for the one hero per page that needs to feel premium; use the static texture overlays for everything else.

How it works

The implementation has three layers:

1. Section schema fields

All fields live on <x-dl.section> (the wrapper every row uses), registered by app/Support/DlSchemas/Section.php:

Field Type Purpose
section_texture text (preset key) Selects one of the 22 static texture overlays (see v1 above), or '' for none
section_texture_opacity text (0-100) Fades the texture's tuned baked strength. '' or 100 = full strength
section_texture_fade text (enum) Fade direction for line/grid-style textures — '' (center), edges, top, bottom, even
section_bg_video video (path) Storage path of the uploaded video file. Its poster is resolved at render from the video's linked media-library poster — there is no separate poster field.
section_bg_video_overlay_classes classes Optional Tailwind classes for an overlay div on top of the video (e.g. bg-tone-900/40)
section_bg_video_mobile toggle Off (default) skips the video <source> on phones (media="(min-width: 768px)") and shows the responsive poster image instead; on plays the video everywhere
section_animated_canvas text (preset key) Selects one of the canvas presets — Canvas 2D: particle-field, gradient-orbs, lattice-waves, color-sweep, constellation, halftone-shift, bokeh-drift, fireflies, wave-ribbons; WebGL: night-sky, aurora-veil
section_animated_canvas_overlay_classes classes Optional Tailwind classes for an overlay div on top of the canvas. In the editor this is set through the overlay tint composer (a color swatch + strength slider that writes bg-black/40-style utilities); a "Type classes instead" escape hatch accepts arbitrary classes. Only renders when a canvas preset is also set.
section_animated_canvas_params text (JSON) Per-preset tweakable parameters as a JSON object (e.g. {"density":1.5,"direction":"down","fade":"none"}). The editor's row paintbrush renders one control per declared param (slider or dropdown) under the canvas accordion. Numeric values feed WebGL u_{key} uniforms; the whole object is exposed to Canvas 2D presets as state.params; the fade key drives a server-side CSS mask. Garbage values fall back to the preset's declared default; the attribute is omitted entirely on malformed JSON.

Any of the texture/video/canvas values can also be set at the section-preset level (preset['texture'], preset['bg_video'], preset['animated_canvas'], …) so every row using that preset inherits the same background unless a row sets its own value — see $ownTexture / $sectionTexture in section.blade.php for the resolution order.

2. The Section blade renders the decorations

resources/views/components/dl/section.blade.php emits absolute-positioned decoration <div> (or <video> / <canvas>) elements as the first children of the section, behind the content container:

  • Video bg (-z-20) — full-bleed <video> with autoplay/loop/muted/playsinline + a responsive poster <img> layer resolved from the video's linked MediaItem poster, plus an optional overlay div at -z-10
  • Canvas bg (-z-20) — full-bleed <canvas data-anim-preset="..."> lazy-bound by resources/js/animated-bg/
  • Texture decoration (-z-10) — the static CSS/SVG pattern for the selected section_texture, optionally wrapped in an opacity <div> (section_texture_opacity) and a mask-image <div> (section_texture_fade via Section::textureMaskCss())

When any decoration (texture, video, canvas, or a shape divider) is set, the section's class list is augmented with relative isolate overflow-hidden so the negative-z children stack correctly behind content without escaping the section.

3. Editor row paintbrush UI

All three tiers surface in the row's paintbrush ("Design") panel under the Style tab, as sub-accordions inside the Section Background group:

  • v1 (Texture) — a "Texture" sub-accordion: a <select> populated from Section::textureOptions() (writes section_texture), an opacity slider shown once a texture is picked (section_texture_opacity), a fade <select> shown only for the line/grid textures listed in Section::textureCenterMaskStops() (section_texture_fade), and a "Preview options" eye button that opens a live per-texture visual picker (open-section-option-preview, kind: 'texture'). Adding a new texture is one entry in Section::textureOptions() plus a default => branch in section.blade.php's match block.
  • v2 (Video) — a "Video" sub-accordion with: pick-from-media-library (video-only), a video thumbnail preview with a remove button, the overlay-tint composer for section_bg_video_overlay_classes, and a "Play video on mobile" checkbox for section_bg_video_mobile (shown once a video is picked). The poster is managed on the video in the media library (a note in the accordion points there), so there's no per-section poster picker. The video picker opens the editor's pure-Alpine media picker pre-filtered to video-only.

The Alpine media picker's kindFilter prop is plumbed end-to-end:

  • EditorMediaActions::openRowDesignVideoPicker dispatches request-open-media-picker with kindFilter: 'video'
  • EditorModals::onRequestOpenMediaPicker forwards it to the open-media-picker-modal window event
  • The Alpine factory in resources/js/editor/media-picker-modal.js passes it to the server RPCs (loadMediaPickerInitialPayload, loadMediaPickerPagePayload) as a query filter
  • The picker shell template branches per-item between <img> and <video> based on the item's kind

Texture rendering details

Every texture resolves through the same match ($sectionTexture) block in section.blade.php, landing on one of two implementations:

Mechanism Textures How it reads on dark backgrounds
Tailwind arbitrary-value gradient, dark: variant grid, graph-paper, dots, diagonal-lines, crosshatch A second dark:bg-[...] declaration swaps in a darker --color-tone-* line/dot colour
Tailwind arbitrary-value gradient, dual-tone rgba() emboss isometric, diagonal-hatch, concentric A black layer and a white layer at low opacity stack in the same gradient, so no dark: variant is needed
Single radial rgba(0,0,0,…) fade vignette N/A — it's a corner-darkening vignette, not a colour-adaptive pattern
Inline SVG <pattern> (Section::{name}Svg()) blueprint, plus-marks, hex-mesh, triangle-mesh, brick, herringbone, chevron, fish-scale, quatrefoil, topographic, circuit, sparkles, mosaic, pixel-mosaic, pixel-mosaic-full Most use the same black+white dual-tone emboss layering as the CSS group; several (pixel-mosaic, pixel-mosaic-full, mosaic) additionally randomize per-cell fill/opacity from a fixed mt_srand() seed, so the generated SVG markup is byte-stable across renders despite looking hand-scattered

section_texture_opacity wraps the whole decoration in an outer <div style="opacity:…"> (skipped when ''/100). section_texture_fade resolves through Section::textureMaskCss() into a mask-image / -webkit-mask-image wrapper <div> — a radial center mask by default (tuned per-texture stops in Section::textureCenterMaskStops()), or a directional linear-gradient mask for top/bottom, or none for even. Textures not listed in textureCenterMaskStops() (vignette, pixel-mosaic, pixel-mosaic-full) carry their own intrinsic shape and ignore the fade control entirely.

Nothing here animates — there's no @keyframes, no will-change, no prefers-reduced-motion handling, because there's no motion to reduce.

Video performance and best practices

For editor guidance on uploading effective background videos:

  • Encoding. MP4 with H.264 video codec is the universal pick — every modern browser plays it. WebM/VP9 produces smaller files but has gaps on iOS Safari versions <13.
  • Length. Hero loops should be 5–15 seconds — long enough to feel alive, short enough that the seam is hard to notice.
  • Resolution. 1920×1080 is the sweet spot for hero backgrounds. Going to 4K typically isn't worth the bandwidth — the user is going to scroll past it in a few seconds.
  • Bitrate. Aim for under 5 Mbps for a 10-second loop, which lands around 6 MB encoded. Files over 50 MB will feel sluggish on first paint even with preload="metadata".
  • Audio. Always strip the audio track before upload. The browser will autoplay the video muted regardless, but the audio bytes are dead weight on the wire.
  • Loop seamlessness. Hand-edit the first and last frames to match so the loop point doesn't visually pop. Avoid videos with cuts or scene changes — they read as "broken" when looped.
  • Poster image. The system auto-extracts a poster from frame 0 at upload time, so most editors don't need to think about this. If you do want a custom poster (e.g. the first frame is too dark or empty), open the video's Media Library detail panel and use Regenerate from video / Upload / Remove — there is no per-section poster picker.
  • Mobile. Verify on a real mobile device before shipping. Some iOS versions still drop autoplay if the browser tab isn't visible at decode time; playsinline + muted are required (the renderer sets both automatically).

Storage model

Videos use the same year/month folder layout as images (storage/app/public/{Y}/{m}/file.mp4) and live in the same media_items table. A new kind column (image | video) was added in migration 2026_05_26_201116_add_kind_to_media_items_table; existing rows are backfilled to image. The column is indexed so picker queries filter cheaply.

A second migration (2026_05_26_204745_add_poster_media_id_to_media_items_table) adds a self-referential poster_media_id FK so a video MediaItem can point at the image MediaItem auto-extracted from its first frame. nullOnDelete is set on the FK so deleting the poster leaves the video usable (the video keeps playing; the bg-video field just needs a new poster picked manually). The relationship is exposed as MediaItem::poster() and surfaced on the picker payload as posterMediaId so the editor can resolve the link without a second query.

Videos bypass the on-upload ImageResizer::resizeAbsolutePath step (the image resizer can't read video bytes and would corrupt them) and the getimagesize dimension probe (returns false for video). They therefore have null width/height. mime_type is set from the file's reported mime; kind is set from the video/ mime prefix.

Replace / rename / delete behaviour matches images: replacing keeps the path and updates the file in place (consumers pick up the new bytes via the ?v={updated_at} cache buster on url()); deleting nulls path-based references in content_overrides and lets the FK delete cascade kill pivot rows.

What's not included

These were considered and deliberately deferred or left out:

  • Server-side video transcoding (e.g. ffmpeg). Posters are extracted client-side at upload (canvas-based, ~250ms, no system dependency). Transcoding videos to alternate codecs / bitrates / resolutions on the server would require ffmpeg as a system binary, plus a queue + retry layer — out of scope for the current need. Editors who want optimised files can encode locally before upload.
  • Fluid simulation shader preset. Considered for v5 but deferred — a real fluid sim needs multi-pass framebuffer ping-pong + several buffer textures, which would balloon the runtime well beyond the current ~5.5 KB gzipped budget. The v5 WebGL presets cover the premium-hero look with single-pass fragment shaders. Revisit if there's specific demand.
  • Three.js or other shader libraries. Hand-rolled WebGL2 setup is ~1 KB of code; pulling in Three.js for the shader runtime would add ~80 KB gzipped for marginal benefit on what are fundamentally 2D fullscreen fragment shaders.
  • Custom shader authoring in the editor. Picking a preset is the only entry point — editors can't write or paste GLSL. Custom CSS via the section's Custom CSS field is the escape hatch for unusual visual needs.
  • YouTube / Vimeo background loops. Out of scope — they require third-party consent gating, can't be reliably muted on iOS Safari, and can't have a controlled aspect ratio. Use the v2 self-hosted path instead.
  • Multiple stacked video layers. Each section gets one bg-video. Layering multiple is conceptually possible but the UX cost (more pickers, more state) outweighs the use case.
  • Per-texture tuning beyond opacity and fade. The texture tier exposes only section_texture_opacity and section_texture_fade — no per-texture colour, scale, or spacing controls; editors who need a different look pick a different texture, or override via the section's Custom CSS field. (The canvas presets are the opposite call — every one of them ships several per-section parameter controls, since motion, density, and color are the whole point of picking one.)

Files touched

For maintainers — the canonical implementation surface:

File Purpose
app/Support/DlSchemas/Section.php Schema fields, textureOptions(), textureFadeOptions(), textureCenterMaskStops(), textureMaskCss(), the per-texture {name}Svg() builders, canvasPresetOptions() / canvasPresetParams(), mime → kind helpers
resources/views/components/dl/section.blade.php Decoration rendering (video, canvas, static texture)
resources/css/public.css Overlay-opacity safelist consumed by the video/canvas overlay-tint composer (no texture-specific CSS lives here — texture classes/SVG are emitted inline per render)
app/Models/MediaItem.php kind column fillable, kindForMime() + isVideo() helpers, video-aware createForStoragePath(), rename regex
database/migrations/2026_05_26_201116_add_kind_to_media_items_table.php Adds kind enum-style column with indexed default
database/migrations/2026_05_26_204745_add_poster_media_id_to_media_items_table.php Adds self-referential poster_media_id FK
app/Http/Controllers/MediaUploadController.php Editor's direct-upload endpoint: accepts video mimes + paired posters, 100 MB cap
resources/views/pages/dashboard/media-library/⚡{index,picker}.blade.php Standalone library page + legacy Livewire picker: video upload + grid + edit modal
app/Support/Rows/RowFieldParser.php sectionExcludedKeys() / sectionDesignKeys() — derived from Section::schemaFields(), so section_texture* and the video/canvas fields are excluded from content-sidebar hydration automatically
app/Concerns/EditorMediaActions.php openRowDesignVideoPicker + companion picker/remove/apply methods
app/Concerns/EditorMediaPickerActions.php RPC kindFilter param + kind / mimeType in item payload
app/Livewire/EditorModals.php Forwards kindFilter from request event to picker open event
resources/js/editor/media-picker-modal.js Alpine state kindFilter, plumbed through handleOpen + page loads; client-side extractVideoFirstFrame + paired poster upload
resources/js/editor/editor-state.js Routes row-design-bg-video: and row-design-bg-video-poster: picker keys
resources/views/livewire/editor-modals-partials/media-picker-shell.blade.php Renders <video> per item + dynamic accept attr
resources/views/pages/dashboard/pages/⚡editor.blade.php "Texture" sub-accordion + "Video" sub-accordion + "Animated canvas" picker + section_* allowlist sync
resources/js/animated-bg/ Canvas 2D (v3, v3.1) + WebGL2 (v5) runtime. index.js is the entry (lifecycle, painters, lazy preset loader); shared.js + shared-glsl.js hold runtime helpers; presets/{name}.js is one ES module per preset. Vite emits one async chunk per preset via import.meta.glob('./presets/*.js'), so pages only download chunks for presets actually present in the DOM (~1-2 KB per preset on top of the ~8 KB index, vs the old ~30 KB monolithic runtime).
resources/js/public.js Detects canvas[data-anim-preset] on DOMContentLoaded and dynamic-imports the runtime
tests/Feature/SectionTextureFadeTest.php v1 texture fade-mask + option-set render tests
tests/Feature/SectionBgVideoTest.php v2 video bg render tests + kindForMime
tests/Feature/MediaUploadControllerVideoPosterTest.php v2.1 paired upload + poster_media_id link tests
tests/Feature/SectionAnimatedCanvasTest.php v3 canvas preset render + allowlist + dataset attr tests