From 9dda6ca36b8ba69b5d79e85682721d3cd6977643 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 27 Feb 2026 11:43:10 -0500 Subject: [PATCH] docs: add sandbox mode architecture and filter reference Architecture diagrams, export pipeline, face detection pipeline, filter preset reference table, CORS requirements, localStorage keys, mobile behavior, and stacking with shop color filter. --- docs/sandbox-mode.md | 323 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/sandbox-mode.md diff --git a/docs/sandbox-mode.md b/docs/sandbox-mode.md new file mode 100644 index 0000000..033679a --- /dev/null +++ b/docs/sandbox-mode.md @@ -0,0 +1,323 @@ +# Sandbox Mode: Creative Filter System + +Client-side creative toolkit for MPS shops. When a shop owner enables sandbox +mode, all visitors see a floating filter toolbar. Users can apply real-time +CSS/SVG/canvas filters to images and video, export filtered artifacts, and +activate face detection overlays. All processing runs in the browser. The +server stores only the boolean toggle. + +## Architecture + +``` +┌──────────────────────────────────────────────────────────┐ +│ Browser (client) │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ CSS Filters │ │ Canvas Export │ │ MediaPipe │ │ +│ │ (live view) │──>│ (artifacts) │ │ Face Mesh │ │ +│ │ │ │ │ │ (on-demand) │ │ +│ │ 32 presets │ │ ctx.filter │ │ 468 landmarks│ │ +│ │ 7 sliders │ │ toBlob() │ │ eye glow │ │ +│ │ SVG filters │ │ MediaRecorder│ │ face mask │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ │ ┌─────┴─────┐ │ │ +│ │ │ Download │ │ │ +│ │ │ or Upload │ │ │ +│ │ │ (presign) │ │ │ +│ │ └───────────┘ │ │ +│ │ │ │ +│ ┌──────┴──────────────────────────────────────┴───────┐ │ +│ │ sandbox.js (IIFE, ~700 lines) │ │ +│ │ localStorage persistence │ window.sandboxReapply │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────┬──────────────────────────────┘ + │ + ┌────────┴────────┐ + │ MPS Server │ + │ │ + │ shop.sandbox │ + │ _mode = True │ + │ │ + │ (that's it. │ + │ server stores │ + │ one boolean.) │ + └─────────────────┘ +``` + +## Filter Pipeline + +``` +User clicks preset User adjusts slider + │ │ + ▼ ▼ +┌─────────────┐ ┌──────────────────┐ +│ PRESETS[idx] │ │ sliderValues[id] │ +│ .css string │ │ → fn(val+unit) │ +└──────┬──────┘ └────────┬─────────┘ + │ │ + └────────────┬─────────────┘ + │ + ▼ + ┌──────────────────┐ + │ getCurrentFilter │ CSS filter string: + │ CSS() │──> "sepia(40%) contrast(90%) + └──────────────────┘ brightness(105%) saturate(80%)" + │ + ▼ + ┌──────────────────┐ + │ element.style │ Applied to: img, video + │ .filter = css │ (NOT — stacks with + └──────────────────┘ shop color_filter) +``` + +## Export Pipeline + +### Image Export + +``` +img.product-main + │ + ▼ + new Image() + crossOrigin = "anonymous" ← CORS required from CDN + │ + ▼ + canvas.getContext("2d") + ctx.filter = getCurrentFilterCSS() + ctx.drawImage(img, 0, 0) + │ + ▼ + canvas.toBlob("image/png") + │ + ├──> Download (a.download = "sandbox-export.png") + │ + └──> Upload (presigned POST to S3) [future] +``` + +### Video Export + +``` +video element + │ + ▼ + canvas.captureStream(30fps) + │ + ▼ + MediaRecorder(stream, "video/webm") + │ + ▼ + requestAnimationFrame loop: + ctx.filter = getCurrentFilterCSS() + ctx.drawImage(video, 0, 0) + │ + ▼ + recorder.stop() → Blob + │ + └──> Download ("sandbox-export.webm") +``` + +## Face Detection Pipeline (MediaPipe) + +``` +User clicks "Load Face Detection" + │ + ▼ + Load face_mesh.js from CDN (~4MB WASM) + Load camera_utils.js from CDN + │ + ▼ + FaceMesh({ maxNumFaces: 4, refineLandmarks: true }) + │ + │ + ┌────┴────────────────────┐ + │ │ + ▼ ▼ +Image path Video path + │ │ + draw to canvas Camera utility + faceMesh.send() onFrame → send() + │ │ + ▼ ▼ + onResults(landmarks) onResults(landmarks) + │ │ + └────────┬────────────────┘ + │ + ▼ + 468 face landmarks per face + │ + ┌──────┴──────┐ + │ │ + ▼ ▼ + Eye Glow Face Mask + │ │ + │ Iris │ Face outline + │ landmarks │ (landmarks + │ 468-477 │ 10,338,297...) + │ │ + │ Radial │ Fill face oval + │ gradient │ Draw eye shapes + │ overlay │ Draw eyebrows + │ │ Draw mustache + │ │ Draw goatee + ▼ ▼ + Canvas overlay composited + on top of media element +``` + +## Filter Preset Reference + +### Basic (4) +| Name | CSS Filter Chain | +|------|-----------------| +| None | `none` | +| Grayscale | `grayscale(100%)` | +| Sepia | `sepia(100%)` | +| Invert | `invert(100%)` | + +### Warm (4) +| Name | CSS Filter Chain | +|------|-----------------| +| Warm | `sepia(30%) saturate(140%) brightness(105%)` | +| Sunset | `sepia(50%) hue-rotate(-15deg) saturate(150%)` | +| Golden | `sepia(60%) brightness(110%) contrast(90%)` | +| Amber | `sepia(40%) saturate(160%) hue-rotate(-10deg) brightness(108%)` | + +### Cool (4) +| Name | CSS Filter Chain | +|------|-----------------| +| Cool | `sepia(20%) hue-rotate(180deg) saturate(120%)` | +| Arctic | `sepia(30%) hue-rotate(200deg) brightness(110%)` | +| Moonlight | `sepia(40%) hue-rotate(220deg) saturate(80%) brightness(105%)` | +| Frost | `sepia(25%) hue-rotate(190deg) brightness(115%) contrast(95%)` | + +### Dramatic (4) +| Name | CSS Filter Chain | +|------|-----------------| +| Hi-Contrast | `contrast(180%) saturate(120%)` | +| Noir | `grayscale(100%) contrast(150%) brightness(90%)` | +| Faded | `contrast(80%) saturate(60%) brightness(110%)` | +| Vintage | `sepia(40%) contrast(90%) brightness(105%) saturate(80%)` | + +### Color (5) +| Name | CSS Filter Chain | +|------|-----------------| +| Red Shift | `sepia(100%) hue-rotate(-30deg) saturate(300%)` | +| Green Shift | `sepia(100%) hue-rotate(85deg) saturate(300%)` | +| Blue Shift | `sepia(100%) hue-rotate(200deg) saturate(300%)` | +| Purple Haze | `sepia(100%) hue-rotate(265deg) saturate(200%)` | +| Cyan | `sepia(100%) hue-rotate(145deg) saturate(300%)` | + +### Instagram-Style (7) +| Name | CSS Filter Chain | +|------|-----------------| +| Clarendon | `contrast(120%) saturate(125%)` | +| Juno | `sepia(10%) contrast(110%) saturate(150%) brightness(105%)` | +| Lark | `contrast(90%) brightness(115%) saturate(85%)` | +| Gingham | `brightness(105%) hue-rotate(350deg) saturate(80%)` | +| Nashville | `sepia(25%) contrast(120%) brightness(105%) saturate(120%) hue-rotate(-15deg)` | +| Valencia | `sepia(15%) contrast(110%) brightness(108%) saturate(120%)` | +| Walden | `brightness(110%) saturate(160%) sepia(30%) hue-rotate(350deg)` | + +### SVG Filters (4) +| Name | Applied As | +|------|-----------| +| Duo Blue | `url(#sandbox-duotone-blue)` — SVG feColorMatrix | +| Duo Green | `url(#sandbox-duotone-green)` — SVG feColorMatrix | +| Film Grain | `url(#sandbox-grain)` — SVG feTurbulence + feBlend | +| Vignette | `url(#sandbox-vignette)` — SVG feFlood + feGaussianBlur | + +## Adjustment Sliders + +| Slider | Range | Default | CSS Function | +|--------|-------|---------|-------------| +| Brightness | 0-200% | 100% | `brightness()` | +| Contrast | 0-200% | 100% | `contrast()` | +| Saturation | 0-300% | 100% | `saturate()` | +| Hue | 0-360deg | 0deg | `hue-rotate()` | +| Blur | 0-20px | 0px | `blur()` | +| Sepia | 0-100% | 0% | `sepia()` | +| Grayscale | 0-100% | 0% | `grayscale()` | + +## Shop Owner Configuration + +Enable in **Shop Settings > Announcement Ribbon Settings** (the form section +that contains display toggles): + +``` +Settings > ribbon-settings form section > Sandbox Mode > Enable/Disable +``` + +The toggle adds `sandbox_mode = True` to the shop model. The base template +conditionally renders the toolbar HTML, SVG filter definitions, and loads +`sandbox.js` only when enabled. + +## Watch Mode SPA Integration + +When watch mode is enabled, SPA navigation replaces media elements without a +full page reload. The `updatePageContent()` function in `watch.js` calls +`window.sandboxReapply()` after swapping DOM elements. This re-applies the +active sandbox filter to newly loaded images and video with a 50ms delay to +let the DOM settle. + +## CORS Requirements for Canvas Export + +CSS filter preview works without CORS — filters are applied as rendering hints +on the element. Canvas export (toBlob) requires pixel access, which requires +CORS headers from the CDN. + +DigitalOcean Spaces CORS configuration needed: + +```json +{ + "CORSRules": [{ + "AllowedOrigins": ["*"], + "AllowedMethods": ["GET"], + "AllowedHeaders": ["*"], + "MaxAgeSeconds": 3600 + }] +} +``` + +Without CORS, the export button shows a user-friendly error message. Filter +preview continues to work normally. + +## localStorage Keys + +| Key | Value | Purpose | +|-----|-------|---------| +| `mps_sandbox_preset` | Integer (0-31) | Active preset index | +| `mps_sandbox_sliders` | JSON object | Slider values | +| `mps_sandbox_panel` | "0" or "1" | Panel open/closed | +| `mps_sandbox_mode` | "preset" or "custom" | Filter mode | + +## Files + +| File | Role | +|------|------| +| `models/shop.py` | `sandbox_mode` Boolean column | +| `views/shop.py` | Toggle handler in ribbon-settings | +| `templates/shop_settings.j2` | Radio buttons for enable/disable | +| `templates/base.j2` | Conditional toolbar HTML + SVG defs + script tag | +| `static/js/sandbox.js` | Client-side filter engine | +| `static/css/common.css` | Toolbar layout (CSS Grid) | +| `static/js/watch.js` | `sandboxReapply()` hook in SPA navigation | +| `scripts/alembic/versions/a2c13d3117f2_*.py` | Migration | + +## Mobile Behavior + +On screens `max-width: 800px`: +- Toggle button: bottom-right corner (10px offset) +- Panel: full-width (10px margin left + right) +- Max height: 50vh (scrollable) +- All controls always visible (no hover-only interactions) +- Touch targets adequate for finger tapping + +## Stacking with Shop Color Filter + +The shop-level `color_filter` (grayscale, red, green, blue, etc.) applies to +`` via `data-color-filter` attribute. Sandbox filters apply to individual +`img` and `video` elements via `element.style.filter`. These stack: the shop +filter colors the entire page, and the sandbox filter adds per-element effects +on top. This is intentional — the two systems are independent and composable.