diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c2c9..b324952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ per Windows (portatile e installabile), Mac e Android, e va online su [GitHub Pa - **Clear** (licenza source-available, non permissiva) si può spegnere dal Centro AI. - **Nota onesta**: i modelli veri non sono stati scaricati né eseguiti; prove con motori finti (`test/unit/funzioni-ai.test.mjs`, `test/unit/motore_ai_server*.py`). +## [1.8.7] — 2026-10-30 · Zoom sul cursore come si deve, README nuovo + +- **Zoom che segue il cursore**: quando muovi tanto il mouse si vede **tutto lo schermo**; si avvicina solo quando ti fermi o ti muovi piano, e dopo un movimento forte aspetta un attimo di calma prima di zoomare di nuovo. Quando è vicino segue più svelto e **il cursore non esce mai dall'inquadratura**. +- **README nuovo**, sul modello del DaP Convertitore: copertina, pulsanti di download, funzioni e foto vere del programma (`scripts/readme-immagini.mjs` le rifà). + ## [1.8.6] — 2026-10-30 · Stabile: niente più RAM e VRAM piene, zoom sul cursore rifatto - **RAM e VRAM non si riempiono più** dopo una registrazione LIVE: il monitor e la striscia di miniature della timeline aprivano decine di decoder al secondo quando chiedevano un istante prima del primo fotogramma. Corretto, e in più una **sentinella** mette in attesa i lavori di sfondo se i decoder aperti diventano troppi. diff --git a/README.md b/README.md index 8c8710b..81df12b 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,112 @@

- DaProd Editor Video
- Un editor video per Windows e Mac, in stile banco di montaggio.
- Lo tieni sul tuo PC. Niente account, niente abbonamento. + Entra nella community DaProd su Discord

- Versione - Offline - Gratis - Licenze di terzi + DaP Editor Video — vecchio stile, moderno dentro

-
- -

Scarica

-

- Scarica per Windows, installer - Scarica per Windows, portatile - Scarica per Mac + Un banco di montaggio per Windows e Mac, nello spirito delle centraline a nastro.
+ Monti, registri lo schermo, ritocchi il colore ed esporti: tutto sul tuo PC, senza account e senza abbonamenti.

-

Dalla pagina delle release scegli il file della tua versione:
-DaProd-Video-<versione>-setup.exe (installer Windows) · DaProd-Video-<versione>-portatile.exe (Windows senza installazione) · DaProd-Video-<versione>.dmg (Mac).

- -

Windows può dire «Windows ha protetto il PC»: premi Ulteriori informazioni → Esegui comunque. Il Mac, la prima volta: tasto destro → Apri. I file non sono firmati a pagamento.

+

+ Versione + Tutto in locale + Gratis + DaProdStore +


-

Cosa fa

- -- 🎬 **Montaggio a tracce**: tagli, spostamenti, dissolvenze, velocità. Il tasto **1** taglia, il tasto **2** elimina. -- 🎛️ **Proprietà a schede**: Quadro, Colore, Audio e AI, sempre nello stesso posto. -- 🔊 **Finale**: volume in LUFS, controllo prima dell'uscita, preset per YouTube, Reel/TikTok, Instagram e master. -- 📡 **LIVE**: registri schermo, webcam e microfono. Scene in diretta (tasti 1–6), telecomando a parte. -- 🪄 **Montage**: scegli i file e lo stile, il programma monta da solo. Ritocchi veloci, e lo stesso seme dà lo stesso montaggio. -- 🎨 **Colore**: LUT `.cube`, ruote lift/gamma/gain, curve RGB. -- 🔑 **Keyframe e maschere**: posizione, zoom, opacità, colore, volume ed effetti. Maschere a rettangolo, ellisse e Bézier. -- 🔤 **Titoli animati**: 24 animazioni pronte. Monitor ed esportazione sono identici. -- 🧠 **AI locali** (vedi sotto): sfondo, rumore, voce, sottotitoli. -- 💾 **Non perdi il lavoro**: autosalvataggio, «Recupera il lavoro» dopo un crash, file `.dap` con i media dentro. - -
+

⬇  Scarica

-

Requisiti

+

+ Scarica l'installer per Windows + Scarica la versione portatile per Windows + Scarica per Mac + Linux, in arrivo +

-- **Windows 10 o 11**, oppure **macOS 14** o più nuovo. -- **Minimo**: un PC Windows o Mac normale. Il montaggio va anche senza scheda video. -- **Per l'AI pesante** (rumore, sfondo, separazione voce): una scheda **NVIDIA RTX** con **8 GB** di memoria video è consigliata. Senza scheda video l'AI gira lo stesso, ma più lenta. -- Spazio libero sul disco: i modelli AI pesano da pochi MB a qualche GB. Li scarichi solo se li vuoi. +

Nella pagina che si apre scegli DaProd-Video-…-setup.exe (installer), …-portatile.exe (senza installazione) o il .dmg per Mac.
Una volta installato, il programma si aggiorna da solo.


-

Primi passi

+

Cosa sa fare

-1. Apri il programma e crea un progetto nuovo (File → Nuovo). -2. Trascina i video nel **Contenitore** a sinistra, poi sulla timeline. -3. Taglia con **1**, togli le parti brutte con **2**, sposta le clip con il mouse. -4. Vai al **Finale**, scegli il preset e fai partire l'esportazione. +

+ Montaggio, LIVE, Montage, colore, AI locale, Finale +


-

Tasti principali

+

Guardalo all'opera

-| Tasto | Cosa fa | -| --- | --- | -| **1** | Taglia (sulle tracce accese, o sulla clip sotto il cursore) | -| **2** | Elimina lo scarto | -| **S** | Separa / unisci | -| **Q / W** | Togli lo scarto | -| **Rotella** | Avanti o indietro di un fotogramma, con il suono | -| **Ctrl + rotella** | Zoom della timeline | -| **Alt + E** | Velocità della clip | -| **Ctrl + D** | Duplica | -| **Ctrl + K** | Cerca in tutto il programma | -| **F1** | Guida | -| **F8** | Montage | -| **F10** | LIVE | - -Tutti i comandi sono in `src/azioni.ts`. **Ctrl + K** li trova anche per nome. +

+ Il banco di montaggio +

+

+ Il Finale con il controllo prima dell'uscita +

+

+ LIVE: registrazione dello schermo con le scene +


-

AI: cosa è pronto e cosa è opzionale

- -**Pronto nel programma** (si scarica il modello la prima volta, poi gira nel programma): +

Le AI sono tue

-- **Togli lo sfondo**: SAM 3 con clic sull'oggetto, o persona. -- **Segui un oggetto**: tracking che sposta testo, immagini ed effetti. -- **Togli i rumori**: MossFormer2 o DeepFilterNet3. -- **Voce e base**: separa voce e musica (Demucs). -- **Sottotitoli automatici**: Whisper, da 40 MB fino a 564 MB. -- **Montaggio automatico** e **colore automatico**: nessun modello, sono regole del programma. +

+ Sottotitoli, voce pulita, sfondo via, colore automatico: girano sul tuo PC, niente viene mandato a nessuno.
+ Quelle pesanti (voce realistica, traduzione, tagli sulle scene) sono opzionali: si installano solo se le scegli,
+ dal Centro AI, e ognuna dice quanto pesa e quanta memoria della scheda video usa. Consigliata una NVIDIA da 8 GB. +

-**Opzionale** (si accende solo se lo chiedi, dal Centro AI): +
-- Trova i tagli automatici, voce pulita, voce e musica con Kim Vocal 2. -- Sottotitoli con Nemotron o con parole e tempi (Qwen3-ASR), traduzione dei sottotitoli (Hy-MT2). -- Voce AI e voce realistica (Qwen3-TTS). -- Voce da podcast (Clear): licenza source-available, si può spegnere. +

Come si installa

-**Sperimentale**: rallentatore fluido (RIFE). +

+ 1 Scarica, 2 Installa, 3 Monta +

-Ogni risultato è materiale che puoi modificare. L'originale resta, e **Ctrl + Z** annulla. +

Windows può dire «Windows ha protetto il PC», perché l'installatore non ha ancora la firma a pagamento.
Premi Ulteriori informazioni → Esegui comunque. Sul Mac, la prima volta: tasto destro sull'app → Apri.


-

Privacy

- -Tutto resta sul tuo PC. I video, l'audio e i progetti non vanno su internet. I modelli AI si scaricano una volta e poi girano in locale. I tuoi file non escono dal PC. Unico contatto esterno: il download dei modelli AI e, se usi Clear, un conteggio d'uso anonimo del suo SDK. +## I tasti che contano -
+| Tasto | Cosa fa | +| --- | --- | +| **1** | taglia sotto la testina | +| **2** | elimina | +| **S** | separa o unisce | +| **Q / W** | tolgono lo scarto prima o dopo la testina | +| **Spazio** | play e stop | +| **Rotella** | un fotogramma alla volta, col suono · **Ctrl + rotella** zoom | +| **Ctrl + K** | cerca qualsiasi comando | +| **F1** | la guida · **F8** Montage · **F10** LIVE | + +## Com'è fatto + +| | | +| --- | --- | +| **L'interfaccia** | **TypeScript** senza framework, grafica «Banco»: metallo scuro, spie che si accendono solo quando serve. | +| **Il motore video** | **WebGPU / WebGL2** per il quadro, **WebCodecs** e [Mediabunny](https://mediabunny.dev) per leggere e scrivere i file: il monitor e l'esportazione fanno lo stesso conto. | +| **L'app** | **Tauri 2 / Rust**: file, progetti, registrazioni, aggiornamenti e il motore AI locale. | +| **Le AI** | Modelli aperti con licenze libere (Apache, MIT), nel programma o nel motore locale opzionale. Elenco completo in [Software di terzi](TERZE-PARTI.md). | -

Licenze di terze parti

+### Come viene sviluppato -Il programma usa componenti di terzi, ognuno con la sua licenza. L'elenco completo e i testi sono in [TERZE-PARTI.md](TERZE-PARTI.md). +Ogni versione ha la sua voce nel [registro delle novità](CHANGELOG.md) e passa da **oltre 330 prove automatiche**. Quando una versione è pronta, una pipeline su GitHub compila Windows e Mac e pubblica la release da sola.

Novità  ·  Software di terzi  ·  - Negozio DaProd  ·  + DaProdStore  ·  Discord

diff --git a/package-lock.json b/package-lock.json index d117730..30e0c00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "daprod-video", - "version": "1.8.6", + "version": "1.8.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "daprod-video", - "version": "1.8.6", + "version": "1.8.7", "license": "MIT", "dependencies": { "@desert-ant-labs/clear": "3.6.0", diff --git a/package.json b/package.json index 6ad3c52..3c19bd7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "daprod-video", "productName": "DaProd Video", - "version": "1.8.6", + "version": "1.8.7", "description": "DaProd Video: il montaggio video vecchio stile, moderno dentro. Timeline, due monitor, pulsantiera e VU a lancetta.", "author": "DaProd", "license": "MIT", diff --git a/risorse/readme/funzioni.png b/risorse/readme/funzioni.png new file mode 100644 index 0000000..a2f8008 Binary files /dev/null and b/risorse/readme/funzioni.png differ diff --git a/risorse/readme/hero.png b/risorse/readme/hero.png new file mode 100644 index 0000000..8ef0618 Binary files /dev/null and b/risorse/readme/hero.png differ diff --git a/risorse/readme/passi.png b/risorse/readme/passi.png new file mode 100644 index 0000000..fbfc2d6 Binary files /dev/null and b/risorse/readme/passi.png differ diff --git a/risorse/readme/scarica-linux.png b/risorse/readme/scarica-linux.png new file mode 100644 index 0000000..f3126a4 Binary files /dev/null and b/risorse/readme/scarica-linux.png differ diff --git a/risorse/readme/scarica-macos.png b/risorse/readme/scarica-macos.png new file mode 100644 index 0000000..e5e5eef Binary files /dev/null and b/risorse/readme/scarica-macos.png differ diff --git a/risorse/readme/scarica-portatile.png b/risorse/readme/scarica-portatile.png new file mode 100644 index 0000000..10ba58f Binary files /dev/null and b/risorse/readme/scarica-portatile.png differ diff --git a/risorse/readme/scarica-win.png b/risorse/readme/scarica-win.png new file mode 100644 index 0000000..f736e71 Binary files /dev/null and b/risorse/readme/scarica-win.png differ diff --git a/risorse/readme/vetrina-1.png b/risorse/readme/vetrina-1.png new file mode 100644 index 0000000..c1dc91c Binary files /dev/null and b/risorse/readme/vetrina-1.png differ diff --git a/risorse/readme/vetrina-2.png b/risorse/readme/vetrina-2.png new file mode 100644 index 0000000..befd08c Binary files /dev/null and b/risorse/readme/vetrina-2.png differ diff --git a/risorse/readme/vetrina-3.png b/risorse/readme/vetrina-3.png new file mode 100644 index 0000000..70be63a Binary files /dev/null and b/risorse/readme/vetrina-3.png differ diff --git a/scripts/readme-immagini.mjs b/scripts/readme-immagini.mjs new file mode 100644 index 0000000..c8c2fd9 --- /dev/null +++ b/scripts/readme-immagini.mjs @@ -0,0 +1,90 @@ +// Disegna le immagini del README (copertina, pulsanti di download, funzioni, passi) in stile «Banco» e fotografa +// l'app vera (build web servita in locale) per la vetrina. Uso: npm run build && node scripts/readme-immagini.mjs +import { chromium } from 'playwright'; +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; + +const OUT = 'risorse/readme'; +fs.mkdirSync(OUT, { recursive: true }); +const BASE = ` +`; + +const spia = (c) => ``; + +function bottone(sistema, titolo, sotto, colore, oro, spento = false) { + const fondo = oro ? 'background:#ffd54a;color:#14110a' : `background:#15141b;border:2px solid ${colore}`; + return `
+
${spento ? '⏳' : '⬇'}
+
${sistema}
+
${titolo}
${sotto}
`; +} + +const FUNZIONI = [ + ['MONTAGGIO', 'Il banco di sempre', '1 taglia, 2 elimina. Niente si copre da solo. Timeline a 60 fps.', '#ffd54a'], + ['LIVE', 'Registra lo schermo', 'Webcam, voce a parte, scene in diretta e zoom che segue il cursore.', '#ff4d6d'], + ['MONTAGE', 'Monta da solo', 'Foto e video, 28 stili, tagli sul battito della musica.', '#35e8ff'], + ['COLORE', 'Da professionisti', 'Colore automatico, curve, LUT, keyframe e maschere.', '#5dffb4'], + ['AI LOCALE', 'Sul tuo PC', 'Sottotitoli, voce pulita, sfondo via, traduzione. Le pesanti sono opzionali.', '#35e8ff'], + ['FINALE', 'Esce perfetto', 'Volume giusto (LUFS), controllo prima dell\'uscita, preset YouTube e Reel.', '#ffd54a'], +]; +const PASSI = [['1', 'Scarica', 'Il setup per Windows o il DMG per Mac.'], ['2', 'Installa', 'Due clic. Si aggiorna da solo.'], ['3', 'Monta', 'Trascina i file sul banco, o premi REC.']]; + +const pagine = { + hero: [1800, 620, `
+
+
DAPROD PRODUZIONI · SALA DI MONTAGGIO
+
DaP Editor Video
+
Vecchio stile, moderno dentro. Monti, registri lo schermo, ritocchi il colore ed esporti: tutto sul tuo PC.
+
+ ${spia('#ff4d6d')}  REC LIVE${spia('#5dffb4')}  60 FPS${spia('#35e8ff')}  AI LOCALE +
+
+
00:12:48:17
+
`], + 'scarica-win': [880, 200, bottone('WINDOWS', 'Scarica l\'installer', 'Setup .exe · si aggiorna da solo', '#ffd54a', true)], + 'scarica-portatile': [880, 200, bottone('WINDOWS', 'Versione portatile', 'Un solo .exe, niente installazione', '#35e8ff', false)], + 'scarica-macos': [880, 200, bottone('MAC', 'Scarica per Mac', 'DMG universale · Apple Silicon e Intel', '#5dffb4', false)], + 'scarica-linux': [880, 200, bottone('LINUX', 'In arrivo', 'Dopo Windows e Mac', '#8d889c', false, true)], + funzioni: [1800, 760, `
${FUNZIONI.map(([e, t, d, c]) => + `
+
${spia(c)}${e}
+
${t}
${d}
`).join('')}
`], + passi: [1800, 330, `
${PASSI.map(([n, t, d]) => + `
+
${n}
${t}
${d}
`).join('')}
`], +}; + +const b = await chromium.launch(); +for (const [nome, [w, h, html]] of Object.entries(pagine)) { + const p = await b.newPage({ viewport: { width: w, height: h } }); + await p.setContent(`${BASE}${html}`, { waitUntil: 'networkidle' }); + await p.screenshot({ path: path.join(OUT, nome + '.png') }); + await p.close(); +} + +// la vetrina: l'app vera, dalla build web +const tipi = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.wasm': 'application/wasm', '.json': 'application/json', '.png': 'image/png' }; +const srv = http.createServer((q, r) => { + let f = path.join('dist', decodeURIComponent(q.url.split('?')[0])); + if (fs.existsSync(f) && fs.statSync(f).isDirectory()) f = path.join(f, 'index.html'); + if (!fs.existsSync(f)) { r.statusCode = 404; return r.end(); } + r.setHeader('Content-Type', tipi[path.extname(f)] ?? 'application/octet-stream'); + fs.createReadStream(f).pipe(r); +}).listen(8979); +const app = await b.newPage({ viewport: { width: 1920, height: 1080 } }); +await app.goto(process.env.APP_URL ?? 'http://127.0.0.1:8979/app/'); +await app.waitForTimeout(3000); +await app.screenshot({ path: path.join(OUT, 'vetrina-1.png') }); +for (const [pagina, file] of [['finale', 'vetrina-2.png'], ['live', 'vetrina-3.png']]) { + await app.evaluate((p) => document.dispatchEvent(new CustomEvent('dpv:pagina', { detail: p })), pagina); + await app.waitForTimeout(1500); + await app.screenshot({ path: path.join(OUT, file) }); +} +await b.close(); +srv.close(); +console.log('immagini del README pronte in', OUT); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 20930e8..1581450 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -615,7 +615,7 @@ checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] name = "daprod-video" -version = "1.8.6" +version = "1.8.7" dependencies = [ "getrandom 0.3.4", "libc", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cafd2e8..dfaa3d6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "daprod-video" -version = "1.8.6" +version = "1.8.7" description = "DaProd Video: il montaggio vecchio stile, moderno dentro" authors = ["DaProd"] license = "MIT" diff --git a/src/core/zoomCursore.ts b/src/core/zoomCursore.ts index 01a48e7..c3cf8f9 100644 --- a/src/core/zoomCursore.ts +++ b/src/core/zoomCursore.ts @@ -80,7 +80,9 @@ const PASSO_MS = 100; /** fermo = il cursore resta in un cerchio di questo raggio (frazione dello schermo) */ const RAGGIO_FERMO = 0.025; const FERMO_MS = 700; -const ZOOM_CLIC = 1.9, ZOOM_FERMO = 1.6, ZOOM_SEGUI = 1.18, ZOOM_MAX = 2.6; +const ZOOM_CLIC = 1.9, ZOOM_FERMO = 1.6, ZOOM_LENTO = 1.35, ZOOM_MAX = 2.6; +/** dopo un movimento forte, quanto deve restare calmo il cursore prima che lo zoom torni */ +const CALMA_MS = 700; /** la finestra più recente letta vicino a tMs (il campionatore la legge ogni ~100 ms) */ function finestraAl(campioni: CampioneCursore[], tMs: number): [number, number, number, number] | null { @@ -105,6 +107,8 @@ export function camminoCamera(dati: DatiCursore, durataMs: number): PuntoCamera[ let cx1 = 0.5, cy1 = 0.5, z1 = 1, cx = 0.5, cy = 0.5, z = 1; let tx = 0.5, ty = 0.5; let ancora: [number, number] | null = null, fermoDa = 0; + // l'ultimo istante di movimento forte: lo zoom torna solo dopo un po' di calma + let agitatoA = -Infinity; let ic = 0; const storia: [number, number, number][] = []; for (let t = 0; t <= durataMs; t += PASSO_MS) { @@ -113,36 +117,49 @@ export function camminoCamera(dati: DatiCursore, durataMs: number): PuntoCamera[ while (ic < dati.clic.length && dati.clic[ic].tMs < t - 1900) ic++; const clic = dati.clic.slice(ic).find((c) => c.tMs <= t + 350); if (q) { - // velocità del cursore negli ultimi 300 ms, in schermi al secondo + // quanta strada ha fatto il cursore nell'ultimo secondo e mezzo, e quanto va veloce adesso storia.push([t, q[0], q[1]]); - while (storia.length && t - storia[0][0] > 300) storia.shift(); - const v = storia.length > 1 ? Math.hypot(q[0] - storia[0][1], q[1] - storia[0][2]) / Math.max(0.1, (t - storia[0][0]) / 1000) : 0; + while (storia.length && t - storia[0][0] > 1500) storia.shift(); + let strada = 0; + for (let i = 1; i < storia.length; i++) strada += Math.hypot(storia[i][1] - storia[i - 1][1], storia[i][2] - storia[i - 1][2]); + const recenti = storia.filter((x) => t - x[0] <= 300); + const v = recenti.length > 1 ? Math.hypot(q[0] - recenti[0][1], q[1] - recenti[0][2]) / Math.max(0.1, (t - recenti[0][0]) / 1000) : 0; + if (v > 0.45 || strada > 0.6) agitatoA = t; + const calmo = t - agitatoA >= CALMA_MS; if (!ancora || Math.hypot(q[0] - ancora[0], q[1] - ancora[1]) > RAGGIO_FERMO) { ancora = q; fermoDa = t; } const fermo = t - fermoDa >= FERMO_MS; const w = fermo ? finestraAl(dati.campioni, t) : null; const ww = w ? w[2] - w[0] : 1, wh = w ? w[3] - w[1] : 1; - if (clic) { tx = clic.cx; ty = clic.cy; tz = ZOOM_CLIC; } + if (!calmo) { tx = 0.5; ty = 0.5; tz = 1; } // si muove tanto: tutto lo schermo + else if (clic) { tx = clic.cx; ty = clic.cy; tz = ZOOM_CLIC; } else if (w && ww * wh < 0.8 && ww * wh > 0.01) { tx = (w[0] + w[2]) / 2; ty = (w[1] + w[3]) / 2; tz = clamp(0.94 / Math.max(ww, wh), 1, ZOOM_MAX); } else if (fermo) { tx = q[0]; ty = q[1]; tz = ZOOM_FERMO; } else { - // in movimento: più è veloce, più si allarga (oltre 1,2 schermi al secondo torna al quadro intero) - tz = 1 + (ZOOM_SEGUI - 1) * clamp(1 - v / 1.2, 0, 1); - // zona morta: la camera si sposta solo se il cursore esce dal 60% centrale dell'inquadratura - const meta = 0.3 / Math.max(1, z); + // movimenti lenti: si resta vicini e si segue con una zona morta piccola + tz = ZOOM_LENTO; + const meta = 0.2 / Math.max(1, z); if (q[0] < cx - meta) tx = q[0] + meta; else if (q[0] > cx + meta) tx = q[0] - meta; else tx = cx; if (q[1] < cy - meta) ty = q[1] + meta; else if (q[1] > cy + meta) ty = q[1] - meta; else ty = cy; } - } else { ancora = null; storia.length = 0; } - const kz = 1 - Math.exp(-PASSO_MS / (tz > z1 ? 320 : 650)); - const kc = 1 - Math.exp(-PASSO_MS / 300); + } else { ancora = null; storia.length = 0; tx = 0.5; ty = 0.5; } + // allontanarsi è svelto (si vede subito tutto), avvicinarsi è morbido + const kz = 1 - Math.exp(-PASSO_MS / (tz > z1 ? 550 : 260)); + const kc = 1 - Math.exp(-PASSO_MS / 160); z1 += (tz - z1) * kz; z += (z1 - z) * kz; cx1 += (tx - cx1) * kc; cy1 += (ty - cy1) * kc; - // la camera non corre: al massimo mezzo quadro inquadrato al secondo - const passo = 0.05 / Math.max(1, z), dx = cx1 - cx, dy = cy1 - cy, d = Math.hypot(dx, dy); + // la camera non scatta: al massimo un quadro inquadrato e mezzo al secondo... + const passo = 0.15 / Math.max(1, z), dx = cx1 - cx, dy = cy1 - cy, d = Math.hypot(dx, dy); const k = d > passo ? passo / d : 1; cx += dx * k; cy += dy * k; + // ...ma il cursore non esce mai dall'inquadratura: se è vicino al bordo la camera lo raggiunge subito + if (q && z > 1.01) { + const bordo = 0.5 / z - 0.06 / z; + if (q[0] < cx - bordo) cx = q[0] + bordo; else if (q[0] > cx + bordo) cx = q[0] - bordo; + if (q[1] < cy - bordo) cy = q[1] + bordo; else if (q[1] > cy + bordo) cy = q[1] - bordo; + cx1 = cx; cy1 = cy; + } out.push({ tMs: t, cx, cy, z }); } return out; diff --git a/test/unit/live-cursore.test.mjs b/test/unit/live-cursore.test.mjs index d73b5bf..1023ebd 100644 --- a/test/unit/live-cursore.test.mjs +++ b/test/unit/live-cursore.test.mjs @@ -290,5 +290,21 @@ test('mouse veloce: la camera non va nel panico (resta larga e si sposta piano)' const d = Z.rimappaCursore(raw, schermo, [{ da: 0, fine: 10000, videoMs: 0, durataMs: 10000 }], 10000); const cam = Z.camminoCamera(d, 10000); assert.ok(cam.every(k => k.z < 1.12), 'resta quasi a quadro intero'); - for (let i = 1; i < cam.length; i++) assert.ok(Math.hypot(cam[i].cx - cam[i - 1].cx, cam[i].cy - cam[i - 1].cy) <= .0501, 'si sposta piano'); + // il cursore resta sempre dentro l'inquadratura + for (const k of cam) { + const q = Z.puntoCursoreAl(d.campioni, k.tMs); + if (q) assert.ok(Math.abs(q[0] - k.cx) <= .5 / k.z + 1e-9 && Math.abs(q[1] - k.cy) <= .5 / k.z + 1e-9, 'cursore fuori quadro a ' + k.tMs); + } +}); + +test('cursore che si muove con lo zoom attivo: mai tagliato fuori, la camera lo segue subito', () => { + const raw = []; + // fermo 2 s al centro (zoom), poi va deciso verso l'angolo in basso a destra + for (let i = 0; i < 120; i++) raw.push({ tMs: i * 1000 / 60, cx: .5, cy: .5, clicks: 0, visible: true }); + for (let i = 120; i < 300; i++) raw.push({ tMs: i * 1000 / 60, cx: .5 + (i - 120) / 400, cy: .5 + (i - 120) / 500, clicks: 0, visible: true }); + const d = Z.rimappaCursore(raw, schermo, [{ da: 0, fine: 5000, videoMs: 0, durataMs: 5000 }], 5000); + for (const k of Z.camminoCamera(d, 5000)) { + const q = Z.puntoCursoreAl(d.campioni, k.tMs); + if (q) assert.ok(Math.abs(q[0] - k.cx) <= .5 / k.z && Math.abs(q[1] - k.cy) <= .5 / k.z, 'tagliato a ' + k.tMs); + } });