- MPS-6: referrer analytics (domain, query, trend line charts) - MPS-7: sandbox mode creative filter system - MPS-8: user S3 bucket + artifact storage - MPS-9: shop S3 mirror bucket - architecture.md: system diagram, request flow, data pipeline, S3 layout - JAVASCRIPT.md: add sandbox.js, signals.js, MediaPipe SDK entries - sandbox-mode.md: mark S3 upload as implemented - mps-2.md: document referrer_domain + referrer_query columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
372 lines
14 KiB
Markdown
372 lines
14 KiB
Markdown
# 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 user's S3 bucket)
|
|
```
|
|
|
|
### 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 |
|
|
|
|
## Artifact Storage (User S3 Bucket)
|
|
|
|
Users can configure their own S3-compatible bucket to upload sandbox artifacts.
|
|
|
|
```
|
|
User Settings (/u/settings)
|
|
│
|
|
│ Artifact Storage form:
|
|
│ - S3 Endpoint URL
|
|
│ - Region (optional)
|
|
│ - Bucket Name
|
|
│ - Access Key
|
|
│ - Secret Key
|
|
│
|
|
▼
|
|
POST /u/settings/storage
|
|
│
|
|
│ Saves credentials on User model
|
|
│ (s3_endpoint, s3_region, s3_bucket,
|
|
│ s3_access_key, s3_secret_key)
|
|
│
|
|
▼
|
|
Sandbox toolbar shows "Upload to Bucket" button
|
|
│ (only when data-has-bucket="1")
|
|
│
|
|
│ 1. User exports image/video (download)
|
|
│ 2. User clicks "Upload to Bucket"
|
|
│ 3. JS POSTs to /u/sandbox/upload
|
|
│ 4. Server generates presigned POST with user's credentials
|
|
│ 5. JS uploads blob directly to user's S3 bucket
|
|
│
|
|
▼
|
|
User's S3 Bucket
|
|
sandbox/{user_id}/{timestamp}-{filename}
|
|
```
|
|
|
|
**Supported services:** DigitalOcean Spaces, AWS S3, MinIO, Backblaze B2,
|
|
any S3-compatible endpoint.
|
|
|
|
**Security:** Credentials stored as plain Unicode columns (same pattern as
|
|
Stripe/PayPal/Adyen keys on Shop model). Secret key field uses `type="password"`
|
|
in the form.
|
|
|
|
## Files
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `models/shop.py` | `sandbox_mode` Boolean column |
|
|
| `models/user.py` | S3 credential columns + `has_s3_bucket` property |
|
|
| `views/shop.py` | Toggle handler in ribbon-settings |
|
|
| `views/user.py` | Storage settings POST handler + S3 fields in settings dict |
|
|
| `views/user_sandbox.py` | Presigned upload endpoint (`/u/sandbox/upload`) |
|
|
| `templates/shop_settings.j2` | Radio buttons for enable/disable |
|
|
| `templates/user_settings.j2` | Artifact Storage form |
|
|
| `templates/base.j2` | Conditional toolbar HTML + SVG defs + `data-has-bucket` |
|
|
| `static/js/sandbox.js` | Client-side filter engine + upload-to-bucket |
|
|
| `static/css/common.css` | Toolbar layout (CSS Grid) |
|
|
| `static/js/watch.js` | `sandboxReapply()` hook in SPA navigation |
|
|
| `routes.py` | `user_storage_settings`, `user_sandbox_upload` routes |
|
|
| `scripts/alembic/versions/a2c13d3117f2_*.py` | Shop sandbox_mode migration |
|
|
| `scripts/alembic/versions/f898ba460612_*.py` | User S3 credentials 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.
|