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.
This commit is contained in:
parent
ed108fe8f5
commit
9dda6ca36b
1 changed files with 323 additions and 0 deletions
323
docs/sandbox-mode.md
Normal file
323
docs/sandbox-mode.md
Normal file
|
|
@ -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 <html> — 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
|
||||
`<html>` 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue