changed to fixed drawing dimensions
This commit is contained in:
parent
239e52dfed
commit
a0e85045d5
182
frontend/app.js
182
frontend/app.js
|
|
@ -5,6 +5,17 @@ const CONFIG = {
|
|||
AUTOSAVE_DELAY: 1000, // ms - Debounce before note save (autoSave) and drawing PNG autosave (_drawingScheduleAutosave)
|
||||
/** Must match drawingRedraw() fill and eraser stroke color (opaque “whiteboard”). */
|
||||
DRAWING_BACKGROUND: '#ffffff',
|
||||
/**
|
||||
* Drawing document size (intrinsic resolution). The display canvas may render
|
||||
* smaller on small screens, but pointer events, ops and the saved PNG always
|
||||
* live in this fixed coordinate space. A future "new drawing" dialog can
|
||||
* override the dimensions per drawing without any architectural change.
|
||||
*/
|
||||
DRAWING_DEFAULT_DOC_W: 1600,
|
||||
DRAWING_DEFAULT_DOC_H: 1000,
|
||||
/** Hard bounds applied when (re)loading or creating a drawing. */
|
||||
DRAWING_MIN_DOC_DIM: 64,
|
||||
DRAWING_MAX_DOC_DIM: 4096,
|
||||
SEARCH_DEBOUNCE_DELAY: 500, // ms - Delay before running note search while typing
|
||||
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
|
||||
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
|
||||
|
|
@ -502,11 +513,17 @@ function noteApp() {
|
|||
currentMedia: '', // Path to current media file (kept as 'currentMedia' for compatibility)
|
||||
currentMediaType: 'image', // 'image', 'audio', 'video', 'document', 'drawing'
|
||||
|
||||
// Drawing canvas (drawing-*.png only) — ops are session-only until Save flattens to PNG
|
||||
// Drawing canvas (drawing-*.png only) — ops are session-only until Save flattens to PNG.
|
||||
// Coordinates in drawingOps[] are stored in DOCUMENT space (drawingDocW × drawingDocH),
|
||||
// not display CSS pixels, so resizing the editor pane never moves strokes and exported
|
||||
// PNGs are byte-deterministic regardless of screen size or device pixel ratio.
|
||||
drawingTool: 'freehand',
|
||||
drawingColor: '#1a1a1a',
|
||||
drawingLineWidth: 4,
|
||||
drawingOps: [],
|
||||
/** Intrinsic drawing dimensions (in document px). Set when a drawing is created or loaded. */
|
||||
drawingDocW: 1600,
|
||||
drawingDocH: 1000,
|
||||
drawingRedoStack: [],
|
||||
drawingDraft: null,
|
||||
drawingIsPointerDown: false,
|
||||
|
|
@ -2811,16 +2828,24 @@ function noteApp() {
|
|||
* Create a blank drawing PNG and open it for editing.
|
||||
* Attachment folder matches "New note" / "New folder" from the same + menu (root vs folder row vs homepage folder).
|
||||
*/
|
||||
async createNewDrawing() {
|
||||
/**
|
||||
* Create a brand-new drawing PNG and open it in the editor.
|
||||
* @param {{docW?: number, docH?: number}} [opts]
|
||||
* Optional intrinsic dimensions in document pixels. Phase-2: a "New drawing"
|
||||
* dialog (presets, A4, custom) only needs to pass these values here — no other
|
||||
* plumbing is required because every downstream function reads doc size from
|
||||
* either the loaded PNG or the in-memory drawingDocW/drawingDocH fields.
|
||||
*/
|
||||
async createNewDrawing(opts = {}) {
|
||||
const targetFolder = this.inferredNewItemTargetFolder();
|
||||
this.closeDropdown();
|
||||
const w = 1200;
|
||||
const h = 675;
|
||||
const w = this._drawingClampDim(opts.docW, CONFIG.DRAWING_DEFAULT_DOC_W);
|
||||
const h = this._drawingClampDim(opts.docH, CONFIG.DRAWING_DEFAULT_DOC_H);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillStyle = CONFIG.DRAWING_BACKGROUND;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
let blob;
|
||||
try {
|
||||
|
|
@ -2848,15 +2873,33 @@ function noteApp() {
|
|||
return this.currentMedia.split('/').map((s) => encodeURIComponent(s)).join('/');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clamp a dimension to the [MIN, MAX] document-px range. Falls back to `fallback`
|
||||
* when value is missing, non-finite or non-positive. Always returns an integer.
|
||||
*/
|
||||
_drawingClampDim(value, fallback) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) return Math.round(fallback);
|
||||
const clamped = Math.min(
|
||||
CONFIG.DRAWING_MAX_DOC_DIM,
|
||||
Math.max(CONFIG.DRAWING_MIN_DOC_DIM, n)
|
||||
);
|
||||
return Math.round(clamped);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a pointer event to DOCUMENT-space coordinates (0..drawingDocW, 0..drawingDocH).
|
||||
* The mapping is independent of CSS size, DPR and zoom — strokes stored from this
|
||||
* function render identically on any device and round-trip cleanly through PNG export.
|
||||
*/
|
||||
_drawingCanvasCoords(event) {
|
||||
const canvas = this._drawingCanvasEl;
|
||||
if (!canvas) return { x: 0, y: 0 };
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = this._drawingCssW / rect.width;
|
||||
const scaleY = this._drawingCssH / rect.height;
|
||||
if (rect.width <= 0 || rect.height <= 0) return { x: 0, y: 0 };
|
||||
return {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY,
|
||||
x: ((event.clientX - rect.left) / rect.width) * this.drawingDocW,
|
||||
y: ((event.clientY - rect.top) / rect.height) * this.drawingDocH,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -2907,19 +2950,29 @@ function noteApp() {
|
|||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Repaint the visible canvas. All drawing math runs in DOCUMENT space; a single
|
||||
* setTransform call maps (docW, docH) → device pixels by combining the doc→display
|
||||
* scale with the device pixel ratio. _drawingDrawOp itself is space-agnostic.
|
||||
*/
|
||||
drawingRedraw() {
|
||||
const canvas = this._drawingCanvasEl;
|
||||
const ctx = this._drawingCtx;
|
||||
if (!canvas || !ctx) return;
|
||||
const w = this._drawingCssW;
|
||||
const h = this._drawingCssH;
|
||||
const dpr = this._drawingDpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const docW = this.drawingDocW;
|
||||
const docH = this.drawingDocH;
|
||||
const cssW = this._drawingCssW;
|
||||
const cssH = this._drawingCssH;
|
||||
const dpr = this._drawingDpr || 1;
|
||||
if (!docW || !docH || !cssW || !cssH) return;
|
||||
const sx = (cssW / docW) * dpr;
|
||||
const sy = (cssH / docH) * dpr;
|
||||
ctx.setTransform(sx, 0, 0, sy, 0, 0);
|
||||
ctx.clearRect(0, 0, docW, docH);
|
||||
ctx.fillStyle = CONFIG.DRAWING_BACKGROUND;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, docW, docH);
|
||||
if (this._drawingBaseImage && this._drawingBaseImage.complete) {
|
||||
ctx.drawImage(this._drawingBaseImage, 0, 0, w, h);
|
||||
ctx.drawImage(this._drawingBaseImage, 0, 0, docW, docH);
|
||||
}
|
||||
for (const op of this.drawingOps) {
|
||||
this._drawingDrawOp(ctx, op);
|
||||
|
|
@ -2982,24 +3035,52 @@ function noteApp() {
|
|||
this._drawingAutosaveTimeout = setTimeout(attemptSave, CONFIG.AUTOSAVE_DELAY);
|
||||
},
|
||||
|
||||
/** Size canvas to drawingCanvasWrap and redraw (fills available pane). */
|
||||
/**
|
||||
* Size the visible canvas as a letterbox of (drawingDocW × drawingDocH) inside the
|
||||
* available wrap, preserving aspect ratio. The display canvas may be smaller than
|
||||
* the wrap on narrow viewports — that is intentional. The document space stays
|
||||
* fixed; only the on-screen viewport scales.
|
||||
*/
|
||||
_drawingLayoutCanvas() {
|
||||
const wrap = this.$refs.drawingCanvasWrap;
|
||||
const canvas = this.$refs.drawingCanvas;
|
||||
if (!wrap || !canvas || this.currentMediaType !== 'drawing') return;
|
||||
let cssW = wrap.clientWidth;
|
||||
let cssH = wrap.clientHeight;
|
||||
if (cssW < 32) cssW = Math.max(320, wrap.parentElement ? wrap.parentElement.clientWidth : 320);
|
||||
if (cssH < 32) {
|
||||
|
||||
// Available area (with sane fallbacks for the brief moment before layout settles).
|
||||
let availW = wrap.clientWidth;
|
||||
let availH = wrap.clientHeight;
|
||||
if (availW < 32) {
|
||||
availW = Math.max(320, wrap.parentElement ? wrap.parentElement.clientWidth : 320);
|
||||
}
|
||||
if (availH < 32) {
|
||||
const col = wrap.closest('.flex-1.flex.flex-col') || wrap.closest('.flex-1');
|
||||
const h = col ? col.clientHeight : 0;
|
||||
cssH = h > 64 ? h : Math.max(240, Math.floor((cssW || 400) * 0.5));
|
||||
availH = h > 64 ? h : Math.max(240, Math.floor((availW || 400) * 0.5));
|
||||
}
|
||||
|
||||
// Letterbox the document into the available area.
|
||||
const docW = this.drawingDocW;
|
||||
const docH = this.drawingDocH;
|
||||
const docAspect = docW / docH;
|
||||
const wrapAspect = availW / availH;
|
||||
let cssW;
|
||||
let cssH;
|
||||
if (wrapAspect > docAspect) {
|
||||
cssH = availH;
|
||||
cssW = cssH * docAspect;
|
||||
} else {
|
||||
cssW = availW;
|
||||
cssH = cssW / docAspect;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.style.width = `${cssW}px`;
|
||||
canvas.style.width = `${cssW}px`;
|
||||
canvas.style.height = `${cssH}px`;
|
||||
canvas.width = Math.round(cssW * dpr);
|
||||
canvas.height = Math.round(cssH * dpr);
|
||||
// Backing store in device pixels — capped above by availW/availH * dpr, so this
|
||||
// never allocates more than the viewport could ever show. Cheap on every device.
|
||||
canvas.width = Math.max(1, Math.round(cssW * dpr));
|
||||
canvas.height = Math.max(1, Math.round(cssH * dpr));
|
||||
|
||||
this._drawingCanvasEl = canvas;
|
||||
this._drawingCssW = cssW;
|
||||
this._drawingCssH = cssH;
|
||||
|
|
@ -3007,7 +3088,6 @@ function noteApp() {
|
|||
if (!this._drawingCtx) {
|
||||
this._drawingCtx = canvas.getContext('2d');
|
||||
}
|
||||
this._drawingCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
this.drawingRedraw();
|
||||
},
|
||||
|
||||
|
|
@ -3029,6 +3109,11 @@ function noteApp() {
|
|||
this.drawingIsPointerDown = false;
|
||||
this._drawingBaseImage = null;
|
||||
this.drawingHasRasterFromFile = false;
|
||||
// Reset to defaults; replaced by the loaded PNG's natural size below.
|
||||
// For drawings created via createNewDrawing() the file already matches the defaults
|
||||
// (or the size requested by a future "new drawing" dialog).
|
||||
this.drawingDocW = CONFIG.DRAWING_DEFAULT_DOC_W;
|
||||
this.drawingDocH = CONFIG.DRAWING_DEFAULT_DOC_H;
|
||||
this._drawingLoadToken = Symbol();
|
||||
const token = this._drawingLoadToken;
|
||||
|
||||
|
|
@ -3064,6 +3149,10 @@ function noteApp() {
|
|||
if (token !== this._drawingLoadToken) return;
|
||||
this._drawingBaseImage = img;
|
||||
this.drawingHasRasterFromFile = true;
|
||||
// Adopt the PNG's intrinsic size as the document size for this drawing.
|
||||
// The PNG itself is the source of truth — no sidecar metadata required.
|
||||
this.drawingDocW = this._drawingClampDim(img.naturalWidth, CONFIG.DRAWING_DEFAULT_DOC_W);
|
||||
this.drawingDocH = this._drawingClampDim(img.naturalHeight, CONFIG.DRAWING_DEFAULT_DOC_H);
|
||||
this._drawingLayoutCanvas();
|
||||
} catch (e) {
|
||||
if (token !== this._drawingLoadToken) return;
|
||||
|
|
@ -3081,16 +3170,22 @@ function noteApp() {
|
|||
return `#${h(r)}${h(g)}${h(b)}`;
|
||||
},
|
||||
|
||||
/** Sample visible canvas color at logical (css) coordinates; sets drawingColor. */
|
||||
/**
|
||||
* Sample visible canvas color under the pointer; sets drawingColor.
|
||||
* Pointer coords come in document space; we map to device pixels via the canvas's
|
||||
* actual backing store (which already includes both the doc→display scale and DPR).
|
||||
*/
|
||||
drawingSampleColor(e) {
|
||||
const canvas = this._drawingCanvasEl;
|
||||
const ctx = this._drawingCtx;
|
||||
if (!canvas || !ctx) return;
|
||||
const docW = this.drawingDocW;
|
||||
const docH = this.drawingDocH;
|
||||
if (!docW || !docH) return;
|
||||
const { x, y } = this._drawingCanvasCoords(e);
|
||||
this.drawingRedraw();
|
||||
const dpr = this._drawingDpr || 1;
|
||||
let ix = Math.floor(x * dpr);
|
||||
let iy = Math.floor(y * dpr);
|
||||
let ix = Math.floor((x / docW) * canvas.width);
|
||||
let iy = Math.floor((y / docH) * canvas.height);
|
||||
ix = Math.max(0, Math.min(ix, canvas.width - 1));
|
||||
iy = Math.max(0, Math.min(iy, canvas.height - 1));
|
||||
const pix = ctx.getImageData(ix, iy, 1, 1).data;
|
||||
|
|
@ -3283,15 +3378,32 @@ function noteApp() {
|
|||
this._drawingSaveInFlight = true;
|
||||
try {
|
||||
this._drawingCancelAutosave();
|
||||
const canvas = this._drawingCanvasEl;
|
||||
const ctx = this._drawingCtx;
|
||||
if (!canvas || !ctx) return;
|
||||
this.drawingDraft = null;
|
||||
this.drawingIsPointerDown = false;
|
||||
// Render onto an off-screen canvas at the exact document size. This makes the
|
||||
// exported PNG byte-deterministic across devices: same ops, same dimensions →
|
||||
// identical bytes regardless of DPR, window size, or zoom level. The off-screen
|
||||
// canvas is allocated per save (microseconds for 1600×1000) so dimension changes
|
||||
// from a future "resize drawing" feature can never leak between saves.
|
||||
const docW = this._drawingClampDim(this.drawingDocW, CONFIG.DRAWING_DEFAULT_DOC_W);
|
||||
const docH = this._drawingClampDim(this.drawingDocH, CONFIG.DRAWING_DEFAULT_DOC_H);
|
||||
const off = document.createElement('canvas');
|
||||
off.width = docW;
|
||||
off.height = docH;
|
||||
const offCtx = off.getContext('2d');
|
||||
if (!offCtx) return;
|
||||
offCtx.fillStyle = CONFIG.DRAWING_BACKGROUND;
|
||||
offCtx.fillRect(0, 0, docW, docH);
|
||||
if (this._drawingBaseImage && this._drawingBaseImage.complete) {
|
||||
offCtx.drawImage(this._drawingBaseImage, 0, 0, docW, docH);
|
||||
}
|
||||
for (const op of this.drawingOps) {
|
||||
this._drawingDrawOp(offCtx, op);
|
||||
}
|
||||
// Keep the visible canvas in sync (cosmetic; UI doesn't have to wait for the upload).
|
||||
this.drawingRedraw();
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
const blob = await new Promise((resolve, reject) => {
|
||||
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
||||
off.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
||||
});
|
||||
this.isSaving = true;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3117,12 +3117,12 @@
|
|||
>
|
||||
<div
|
||||
x-ref="drawingCanvasWrap"
|
||||
class="flex-1 w-full min-h-0 min-w-0"
|
||||
style="min-height: 0;"
|
||||
class="flex-1 w-full min-h-0 min-w-0 flex items-center justify-center"
|
||||
style="min-height: 0; background-color: var(--bg-secondary);"
|
||||
>
|
||||
<canvas
|
||||
x-ref="drawingCanvas"
|
||||
class="drawing-surface block w-full h-full"
|
||||
class="drawing-surface block"
|
||||
:style="drawingTool === 'eyedropper'
|
||||
? 'touch-action: none; cursor: copy; background: #fff;'
|
||||
: 'touch-action: none; cursor: crosshair; background: #fff;'"
|
||||
|
|
|
|||
Loading…
Reference in New Issue