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.
+
-
-
-
-
+
-
-
-
Scarica
-
-
-
-
+ 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.
+
+
+
+
+
+
-
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
+
+
+
+
+
+
-- **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.
+
+
+
-
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.
+
+
+
+
+
+
+
+
+
-
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).
+
+
+
-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.
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);
+ }
});