feat(theseus/screenshot): 0.6.0 — crop + mosaic redaction, right-anchored sidebar controls, real shutter+print sounds
Editor: - Crop tool restored — drag to select, marquee sits with a dashed acid border and a dimmed backdrop for the area you'll discard, then the topbar shows Apply crop / Cancel. Applying trims #base to the rect, resets undo (dimensions changed), and drops back into the select tool. Enter / Esc keyboard shortcuts while a crop is pending. - Blur / mosaic redaction tool back — drag a rectangle, editor downsamples that region of #base to ~12-block granularity and paints the blocks back nearest-neighbour. Commits directly (no confirm step). - Sidebar-window controls (Back, name, Copy, Save, Sound, Maximize) reflow: Back + name on the left, Copy + Save + Sound + Maximize on the right so the "put the sidebar back to normal size" affordance lives where users expect it. Toolbar's drawing tools stay centred. - Back arrow icon swapped from a chevron to a proper flat arrow (line + arrowhead), matching the new browser back/forward glyphs. Sounds — modeled on Firefox Screenshots' feedback rather than beeps: - Shutter is now a real photoshoot click: two mirror-slaps built from a band-passed noise burst (metallic ping) plus a very short square-wave thud each. Sounds like a camera, not a beep. - Copy is a two-chirp "printer feed" — filtered noise burst on top of a sine chirp per beat, staccato ascending pair. Same shape Firefox Easy Screenshot uses for "copied to clipboard". - Save keeps its ascending triad; Discard keeps its descending pair; new small ascending pair for Apply crop. Chrome: - Browser Back / Forward chevrons (M10 3 L5 8 L10 13 — two segments meeting at a point, no shaft) replaced with straight-arrow glyphs (line + arrowhead). Reads as a navigation arrow, not an angle bracket. Bundled but not shipped separately — parent session OTA-signs and pushes.
This commit is contained in:
parent
882de1654f
commit
7405e444e7
6 changed files with 241 additions and 21 deletions
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "screenshot",
|
||||
"name": "Screenshot",
|
||||
"version": "0.5.1",
|
||||
"description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (arrow, rect, ellipse, pen, text, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.",
|
||||
"version": "0.6.0",
|
||||
"description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, rect, ellipse, pen, text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "📸",
|
||||
"main": "index.js",
|
||||
|
|
|
|||
|
|
@ -99,6 +99,15 @@ body { background: var(--bg); color: var(--ink);
|
|||
.stage[data-tool="ellipse"]{ cursor: crosshair; }
|
||||
.stage[data-tool="pen"] { cursor: crosshair; }
|
||||
.stage[data-tool="text"] { cursor: text; }
|
||||
.stage[data-tool="crop"] { cursor: crosshair; }
|
||||
.stage[data-tool="blur"] { cursor: crosshair; }
|
||||
|
||||
/* Apply-crop is the primary action while a crop marquee is up. */
|
||||
#apply-crop {
|
||||
background: var(--acid); color: #101418; border-color: transparent; font-weight: 600;
|
||||
}
|
||||
#apply-crop:hover { filter: brightness(1.06); background: var(--acid); border-color: transparent; }
|
||||
#cancel-crop:hover { border-color: rgba(255,91,91,.5); color: var(--danger); }
|
||||
|
||||
.text-input {
|
||||
position: absolute; z-index: 10;
|
||||
|
|
|
|||
|
|
@ -8,13 +8,22 @@
|
|||
<body>
|
||||
<div class="topbar">
|
||||
<button class="btn" id="back" title="Back to sidebar panel">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M10 3L5 8l5 5"/></svg>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8H3M6 5L3 8l3 3"/></svg>
|
||||
</button>
|
||||
<button class="btn" id="toggle-max" title="Expand the sidebar to full window / restore">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/></svg>
|
||||
</button>
|
||||
<button class="btn" id="toggle-sound" title="Toggle sound"></button>
|
||||
<span class="name" id="name">screenshot</span>
|
||||
|
||||
<!-- Crop mode splits the topbar into a confirm/cancel prompt while the
|
||||
user is dragging a crop rectangle. Everything else in the topbar
|
||||
fades in and out around these two buttons. -->
|
||||
<button class="btn wide" id="apply-crop" hidden title="Apply the crop (Enter)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8l3 3 7-7"/></svg>
|
||||
<span>Apply crop</span>
|
||||
</button>
|
||||
<button class="btn wide" id="cancel-crop" hidden title="Cancel crop (Esc)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<span class="spacer"></span>
|
||||
<button class="btn wide" id="copy" title="Copy PNG to clipboard (Ctrl+C)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="4" width="9" height="10"/><path d="M3 12V3h9"/></svg>
|
||||
|
|
@ -24,12 +33,19 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v9M4 7l4 4 4-4M2 14h12"/></svg>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<button class="btn" id="toggle-sound" title="Toggle sound"></button>
|
||||
<button class="btn" id="toggle-max" title="Expand the sidebar to full window / restore">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="tool active" data-tool="select" title="Select (no tool)">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M3 2l10 5-4 1-1 4z"/></svg>
|
||||
</button>
|
||||
<button class="tool" data-tool="crop" title="Crop (C)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 1v11h11M1 4h11v11"/></svg>
|
||||
</button>
|
||||
<button class="tool" data-tool="arrow" title="Arrow (A)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13L13 3M13 3H8M13 3v5"/></svg>
|
||||
</button>
|
||||
|
|
@ -45,6 +61,9 @@
|
|||
<button class="tool" data-tool="text" title="Text (T)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 3h10M8 3v10M6 13h4"/></svg>
|
||||
</button>
|
||||
<button class="tool" data-tool="blur" title="Blur / mosaic redaction (B)">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="2.5" width="11" height="11"/><path d="M5 6h1M8 6h1M11 6h1M5 9h1M8 9h1M11 9h1M5 12h1M8 12h1M11 12h1"/></svg>
|
||||
</button>
|
||||
|
||||
<span class="sep"></span>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ const $ = (id) => document.getElementById(id);
|
|||
const UNDO_MAX = 25;
|
||||
|
||||
// ---- audio -----------------------------------------------------------
|
||||
// Kept in sync with the sidebar panel: preference stored under
|
||||
// silentmode.storage as "soundOn" (default true), synthesized on the fly
|
||||
// so we don't ship any .wav in the tarball.
|
||||
// Same synth engine as the sidebar panel: preference stored under
|
||||
// silentmode.storage as "soundOn" (default true), no .wav ships in the
|
||||
// tarball. Photoshoot-style shutter for capture, quick "printer chirp"
|
||||
// for copy — modeled on Firefox's Screenshots feedback tones.
|
||||
let soundOn = true;
|
||||
let _audio = null;
|
||||
function _actx() {
|
||||
|
|
@ -43,10 +44,49 @@ function _tone(freq, dur, type, vol, at) {
|
|||
osc.start(t0);
|
||||
osc.stop(t0 + dur + 0.02);
|
||||
}
|
||||
// A short filtered noise burst — the mechanical component of the shutter
|
||||
// click and (paired lower down) the "paper feed" texture of the print
|
||||
// sound. `bp` is the band-pass centre frequency, `q` how tight the band.
|
||||
function _noise(dur, vol, bp, q, at) {
|
||||
if (!soundOn) return;
|
||||
const c = _actx(); if (!c) return;
|
||||
const t0 = c.currentTime + (at || 0);
|
||||
const n = Math.max(1, Math.floor(c.sampleRate * dur));
|
||||
const buf = c.createBuffer(1, n, c.sampleRate);
|
||||
const d = buf.getChannelData(0);
|
||||
for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
|
||||
const src = c.createBufferSource(); src.buffer = buf;
|
||||
const flt = c.createBiquadFilter(); flt.type = "bandpass";
|
||||
flt.frequency.setValueAtTime(bp || 2000, t0);
|
||||
flt.Q.setValueAtTime(q || 4, t0);
|
||||
const g = c.createGain();
|
||||
g.gain.setValueAtTime(vol || 0.2, t0);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||||
src.connect(flt); flt.connect(g); g.connect(c.destination);
|
||||
src.start(t0);
|
||||
src.stop(t0 + dur + 0.02);
|
||||
}
|
||||
// Photoshoot: two clicks — mirror-slap up, mirror-slap down. A short
|
||||
// high-band noise burst is the metallic ping, a low-band burst is the
|
||||
// mechanical thud, and a very short low tone fills in the body of the
|
||||
// shutter closing.
|
||||
function playShutter() {
|
||||
_noise(0.03, 0.28, 4200, 6, 0); // first click, up
|
||||
_tone(180, 0.04, "square", 0.10, 0.005);
|
||||
_noise(0.04, 0.32, 3600, 5, 0.08); // second click, down
|
||||
_tone(140, 0.05, "square", 0.12, 0.09);
|
||||
}
|
||||
// Copy: two-chirp "printer feed" — matches Firefox's Easy Screenshot copy
|
||||
// feedback (staccato ascending pair, no residual hum).
|
||||
function playCopy() {
|
||||
_noise(0.02, 0.14, 3200, 8, 0);
|
||||
_tone(1400, 0.05, "sine", 0.14, 0);
|
||||
_noise(0.02, 0.14, 4200, 8, 0.06);
|
||||
_tone(2000, 0.06, "sine", 0.14, 0.06);
|
||||
}
|
||||
function playSave() { _tone(700, 0.06, "sine", 0.16, 0); _tone(1100, 0.08, "sine", 0.16, 0.05); _tone(1500, 0.09, "sine", 0.16, 0.10); }
|
||||
function playCopy() { _tone(900, 0.06, "sine", 0.15, 0); _tone(1400, 0.08, "sine", 0.15, 0.05); }
|
||||
function playDiscard() { _tone(500, 0.06, "sine", 0.18, 0); _tone(280, 0.10, "sine", 0.14, 0.05); }
|
||||
function playShutter() { _tone(1500, 0.05, "square", 0.20, 0); _tone(600, 0.06, "square", 0.14, 0.03); }
|
||||
function playCropApply() { _tone(1200, 0.05, "sine", 0.14, 0); _tone(1600, 0.07, "sine", 0.14, 0.04); }
|
||||
|
||||
const base = $("base");
|
||||
const over = $("over");
|
||||
|
|
@ -72,6 +112,7 @@ let state = {
|
|||
drag: null, // {x0,y0,x,y}
|
||||
pen: null, // [{x,y}, …]
|
||||
textInput: null, // {x, y, el}
|
||||
cropRect: null, // committed-crop rectangle in canvas coords, {x,y,w,h}
|
||||
};
|
||||
|
||||
let undo = [];
|
||||
|
|
@ -203,12 +244,59 @@ window.addEventListener("resize", () => { if (!stage.hidden) fitBoard(); });
|
|||
|
||||
// -- tool selection --------------------------------------------------------
|
||||
function setTool(name) {
|
||||
// Leaving crop mode with an unapplied marquee drops it.
|
||||
if (state.tool === "crop" && name !== "crop" && state.cropRect) {
|
||||
state.cropRect = null; clearOver();
|
||||
}
|
||||
state.tool = name;
|
||||
stage.dataset.tool = name;
|
||||
for (const b of document.querySelectorAll(".tool")) {
|
||||
b.classList.toggle("active", b.dataset.tool === name);
|
||||
}
|
||||
cancelTextInput();
|
||||
updateCropUI();
|
||||
}
|
||||
|
||||
// Show/hide the Apply crop / Cancel crop buttons in the top bar. Visible
|
||||
// only while the tool is "crop" AND a rectangle has been drawn.
|
||||
function updateCropUI() {
|
||||
const on = state.tool === "crop" && !!state.cropRect;
|
||||
const ac = document.getElementById("apply-crop");
|
||||
const cc = document.getElementById("cancel-crop");
|
||||
if (ac) ac.hidden = !on;
|
||||
if (cc) cc.hidden = !on;
|
||||
}
|
||||
|
||||
// Apply the crop: resize the base canvas to the rect's size, draw the
|
||||
// cropped region onto it, then clear undo/redo (the coordinate system has
|
||||
// changed — pre-crop snapshots would restore into the wrong dimensions).
|
||||
// Baseline snapshot for the cropped canvas becomes the new floor.
|
||||
function applyCrop() {
|
||||
const r = state.cropRect;
|
||||
if (!r) return;
|
||||
const w = Math.max(1, Math.round(r.w));
|
||||
const h = Math.max(1, Math.round(r.h));
|
||||
const x = Math.max(0, Math.round(r.x));
|
||||
const y = Math.max(0, Math.round(r.y));
|
||||
const tmp = document.createElement("canvas");
|
||||
tmp.width = w; tmp.height = h;
|
||||
tmp.getContext("2d").drawImage(base, x, y, w, h, 0, 0, w, h);
|
||||
sizeCanvases(w, h);
|
||||
bctx.drawImage(tmp, 0, 0);
|
||||
state.cropRect = null;
|
||||
clearOver();
|
||||
undo = [];
|
||||
redo = [];
|
||||
pushSnapshot();
|
||||
fitBoard();
|
||||
updateCropUI();
|
||||
playCropApply();
|
||||
setTool("select");
|
||||
}
|
||||
function cancelCrop() {
|
||||
state.cropRect = null;
|
||||
clearOver();
|
||||
updateCropUI();
|
||||
}
|
||||
for (const b of document.querySelectorAll(".tool")) {
|
||||
b.addEventListener("click", () => setTool(b.dataset.tool));
|
||||
|
|
@ -281,6 +369,49 @@ function drawPen(points) {
|
|||
octx.stroke();
|
||||
}
|
||||
|
||||
// Crop overlay — dim the area outside the rect, thin dashed border. This
|
||||
// is a live preview during drag AND the settled marquee while apply-crop
|
||||
// is pending.
|
||||
function drawCropRect(a, b) {
|
||||
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
||||
const w = Math.abs(b.x - a.x), h = Math.abs(b.y - a.y);
|
||||
clearOver();
|
||||
octx.save();
|
||||
octx.fillStyle = "rgba(10,13,19,0.55)";
|
||||
octx.fillRect(0, 0, over.width, over.height);
|
||||
octx.clearRect(x, y, w, h);
|
||||
octx.strokeStyle = "#d6ff3d";
|
||||
octx.lineWidth = 1.5;
|
||||
octx.setLineDash([6, 4]);
|
||||
octx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
|
||||
octx.restore();
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
// Mosaic redaction: downsample the selected region of #base to blocks of
|
||||
// ~<blockSize> px, then paint the blocks back. The whole thing commits
|
||||
// straight to #base (there's no live preview — the drag rect uses the
|
||||
// crop-style marquee to show the target region).
|
||||
function commitBlur(a, b) {
|
||||
const x = Math.max(0, Math.min(a.x, b.x)) | 0;
|
||||
const y = Math.max(0, Math.min(a.y, b.y)) | 0;
|
||||
const w = Math.min(base.width - x, Math.abs(b.x - a.x) | 0);
|
||||
const h = Math.min(base.height - y, Math.abs(b.y - a.y) | 0);
|
||||
if (w < 4 || h < 4) return false;
|
||||
const block = Math.max(6, Math.round(Math.min(w, h) / 12));
|
||||
const tmp = document.createElement("canvas");
|
||||
const scaleW = Math.max(1, Math.floor(w / block));
|
||||
const scaleH = Math.max(1, Math.floor(h / block));
|
||||
tmp.width = scaleW; tmp.height = scaleH;
|
||||
const tctx = tmp.getContext("2d");
|
||||
tctx.imageSmoothingEnabled = true;
|
||||
tctx.drawImage(base, x, y, w, h, 0, 0, scaleW, scaleH);
|
||||
bctx.imageSmoothingEnabled = false;
|
||||
bctx.drawImage(tmp, 0, 0, scaleW, scaleH, x, y, w, h);
|
||||
bctx.imageSmoothingEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function commitOver() {
|
||||
bctx.drawImage(over, 0, 0);
|
||||
clearOver();
|
||||
|
|
@ -307,11 +438,12 @@ base.addEventListener("pointermove", (ev) => {
|
|||
}
|
||||
if (!state.drag) return;
|
||||
state.drag.x = p.x; state.drag.y = p.y;
|
||||
clearOver();
|
||||
const a = { x: state.drag.x0, y: state.drag.y0 }, b = { x: p.x, y: p.y };
|
||||
if (state.tool === "arrow") drawArrow(a, b);
|
||||
else if (state.tool === "rect") drawRect(a, b);
|
||||
else if (state.tool === "ellipse") drawEllipse(a, b);
|
||||
if (state.tool === "arrow") { clearOver(); drawArrow(a, b); }
|
||||
else if (state.tool === "rect") { clearOver(); drawRect(a, b); }
|
||||
else if (state.tool === "ellipse") { clearOver(); drawEllipse(a, b); }
|
||||
else if (state.tool === "crop"
|
||||
|| state.tool === "blur") { drawCropRect(a, b); }
|
||||
});
|
||||
base.addEventListener("pointerup", (ev) => {
|
||||
try { base.releasePointerCapture(ev.pointerId); } catch {}
|
||||
|
|
@ -323,7 +455,21 @@ base.addEventListener("pointerup", (ev) => {
|
|||
}
|
||||
if (!state.drag) return;
|
||||
const dx = state.drag.x - state.drag.x0, dy = state.drag.y - state.drag.y0;
|
||||
if (Math.hypot(dx, dy) < 2) { clearOver(); state.drag = null; return; }
|
||||
if (Math.hypot(dx, dy) < 2) { clearOver(); state.drag = null; state.cropRect = null; updateCropUI(); return; }
|
||||
const a = { x: state.drag.x0, y: state.drag.y0 }, b = { x: state.drag.x, y: state.drag.y };
|
||||
if (state.tool === "crop") {
|
||||
// Don't commit yet — the marquee stays up until the user hits Apply.
|
||||
state.cropRect = drawCropRect(a, b);
|
||||
state.drag = null;
|
||||
updateCropUI();
|
||||
return;
|
||||
}
|
||||
if (state.tool === "blur") {
|
||||
if (commitBlur(a, b)) { clearOver(); pushSnapshot(); }
|
||||
else clearOver();
|
||||
state.drag = null;
|
||||
return;
|
||||
}
|
||||
commitOver();
|
||||
state.drag = null;
|
||||
});
|
||||
|
|
@ -421,6 +567,10 @@ async function copy() {
|
|||
}
|
||||
$("save").addEventListener("click", save);
|
||||
$("copy").addEventListener("click", copy);
|
||||
const applyCropBtn = document.getElementById("apply-crop");
|
||||
const cancelCropBtn = document.getElementById("cancel-crop");
|
||||
if (applyCropBtn) applyCropBtn.addEventListener("click", applyCrop);
|
||||
if (cancelCropBtn) cancelCropBtn.addEventListener("click", cancelCrop);
|
||||
|
||||
// -- top-bar navigation ----------------------------------------------
|
||||
$("back").addEventListener("click", () => { playDiscard(); location.href = "panel.html"; });
|
||||
|
|
@ -473,11 +623,17 @@ window.addEventListener("keydown", (ev) => {
|
|||
if (meta && ev.key.toLowerCase() === "z" && ev.shiftKey) { doRedo(); ev.preventDefault(); return; }
|
||||
if (meta && ev.key.toLowerCase() === "s") { save(); ev.preventDefault(); return; }
|
||||
if (meta && ev.key.toLowerCase() === "c") { copy(); ev.preventDefault(); return; }
|
||||
if (state.tool === "crop" && state.cropRect) {
|
||||
if (ev.key === "Enter") { applyCrop(); ev.preventDefault(); return; }
|
||||
if (ev.key === "Escape") { cancelCrop(); ev.preventDefault(); return; }
|
||||
}
|
||||
if (ev.key === "a") setTool("arrow");
|
||||
else if (ev.key === "r") setTool("rect");
|
||||
else if (ev.key === "o") setTool("ellipse");
|
||||
else if (ev.key === "p") setTool("pen");
|
||||
else if (ev.key === "t") setTool("text");
|
||||
else if (ev.key === "c") setTool("crop");
|
||||
else if (ev.key === "b") setTool("blur");
|
||||
else if (ev.key === "Escape") setTool("select");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -216,8 +216,44 @@
|
|||
osc.start(t0);
|
||||
osc.stop(t0 + dur + 0.02);
|
||||
}
|
||||
function playShutter() { tone(1500, 0.05, "square", 0.20, 0); tone(600, 0.06, "square", 0.14, 0.03); }
|
||||
function playCopy() { tone(900, 0.06, "sine", 0.15, 0); tone(1400, 0.08, "sine", 0.15, 0.05); }
|
||||
// Filtered noise burst — the metallic "click" of the shutter and the
|
||||
// paper-feed texture of the print sound. Band-pass around `bp` with
|
||||
// sharpness `q` picks a resonance so we don't just play white noise.
|
||||
function noise(dur, vol, bp, q, at) {
|
||||
if (!soundOn) return;
|
||||
const c = actx(); if (!c) return;
|
||||
const t0 = c.currentTime + (at || 0);
|
||||
const n = Math.max(1, Math.floor(c.sampleRate * dur));
|
||||
const buf = c.createBuffer(1, n, c.sampleRate);
|
||||
const d = buf.getChannelData(0);
|
||||
for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
|
||||
const src = c.createBufferSource(); src.buffer = buf;
|
||||
const flt = c.createBiquadFilter(); flt.type = "bandpass";
|
||||
flt.frequency.setValueAtTime(bp || 2000, t0);
|
||||
flt.Q.setValueAtTime(q || 4, t0);
|
||||
const g = c.createGain();
|
||||
g.gain.setValueAtTime(vol || 0.2, t0);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
||||
src.connect(flt); flt.connect(g); g.connect(c.destination);
|
||||
src.start(t0);
|
||||
src.stop(t0 + dur + 0.02);
|
||||
}
|
||||
// Photoshoot shutter — mirror-slap up (metallic ping + soft thud), then
|
||||
// mirror-slap down. Reads as a real camera click rather than a beep.
|
||||
function playShutter() {
|
||||
noise(0.03, 0.28, 4200, 6, 0);
|
||||
tone(180, 0.04, "square", 0.10, 0.005);
|
||||
noise(0.04, 0.32, 3600, 5, 0.08);
|
||||
tone(140, 0.05, "square", 0.12, 0.09);
|
||||
}
|
||||
// Copy — two-chirp "printer feed", modeled on Firefox Easy Screenshot's
|
||||
// copy feedback: fast staccato ascending pair, no residual hum.
|
||||
function playCopy() {
|
||||
noise(0.02, 0.14, 3200, 8, 0);
|
||||
tone(1400, 0.05, "sine", 0.14, 0);
|
||||
noise(0.02, 0.14, 4200, 8, 0.06);
|
||||
tone(2000, 0.06, "sine", 0.14, 0.06);
|
||||
}
|
||||
function playDiscard() { tone(500, 0.06, "sine", 0.18, 0); tone(280, 0.10, "sine", 0.14, 0.05); }
|
||||
function paintSound() {
|
||||
btnSound.title = soundOn ? "Sounds on — click to mute" : "Sounds off — click to enable";
|
||||
|
|
|
|||
|
|
@ -394,8 +394,8 @@
|
|||
<div class="tabs" id="tabs"></div>
|
||||
<div class="bar" id="bar">
|
||||
<div class="nav">
|
||||
<button class="ic" id="back" title="Back"><svg viewBox="0 0 16 16"><path d="M10 3 L5 8 L10 13"/></svg></button>
|
||||
<button class="ic" id="fwd" title="Forward"><svg viewBox="0 0 16 16"><path d="M6 3 L11 8 L6 13"/></svg></button>
|
||||
<button class="ic" id="back" title="Back"><svg viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round"><path d="M13 8H3M6 5L3 8l3 3"/></svg></button>
|
||||
<button class="ic" id="fwd" title="Forward"><svg viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h10M10 5l3 3-3 3"/></svg></button>
|
||||
<button class="ic" id="reload" title="Reload"><svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button>
|
||||
<button class="ic" id="home" title="Home"><svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue