added fill bucket
This commit is contained in:
parent
c80e164581
commit
a7995edace
|
|
@ -8,7 +8,7 @@ Use the **+ New** menu in the sidebar and choose **New drawing**. The app create
|
||||||
|
|
||||||
## Editor overview
|
## Editor overview
|
||||||
|
|
||||||
- **Tools** — Freehand pencil, straight line, rectangle, and ellipse; **eraser** (paints with the canvas background color); **eyedropper** to sample a color from the canvas.
|
- **Tools** — Freehand pencil, straight line, rectangle, and ellipse; **eraser** (paints with the canvas background color); **eyedropper** to sample a color from the canvas; **paint bucket** to flood-fill a connected area with the current color (undo/redo supported like any other stroke).
|
||||||
- **Color & stroke width** — Color picker and width slider appear on the same toolbar as the tools.
|
- **Color & stroke width** — Color picker and width slider appear on the same toolbar as the tools.
|
||||||
- **Undo / redo** — Use the main toolbar buttons or **Ctrl+Z** / **Ctrl+Y** (same shortcuts as the note editor; they apply to strokes while a drawing is open).
|
- **Undo / redo** — Use the main toolbar buttons or **Ctrl+Z** / **Ctrl+Y** (same shortcuts as the note editor; they apply to strokes while a drawing is open).
|
||||||
- **Clear** — Replaces the current session with a **blank white image** and schedules a save (see [FEATURES.md](FEATURES.md) for the exact behavior and confirmation).
|
- **Clear** — Replaces the current session with a **blank white image** and schedules a save (see [FEATURES.md](FEATURES.md) for the exact behavior and confirmation).
|
||||||
|
|
|
||||||
211
frontend/app.js
211
frontend/app.js
|
|
@ -3007,6 +3007,12 @@ function noteApp() {
|
||||||
|
|
||||||
_drawingDrawOp(ctx, op) {
|
_drawingDrawOp(ctx, op) {
|
||||||
if (!op) return;
|
if (!op) return;
|
||||||
|
if (op.type === 'fill') {
|
||||||
|
if (op.bitmap) {
|
||||||
|
ctx.drawImage(op.bitmap, op.x, op.y);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (op.type === 'stroke') {
|
if (op.type === 'stroke') {
|
||||||
const pts = op.points;
|
const pts = op.points;
|
||||||
if (!pts || pts.length < 2) return;
|
if (!pts || pts.length < 2) return;
|
||||||
|
|
@ -3091,6 +3097,30 @@ function noteApp() {
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the current drawing state (background + base image + all committed ops) to a
|
||||||
|
* fresh off-screen canvas at the given document size. Used by drawingSave() to produce
|
||||||
|
* a device-independent PNG, and by drawingFill() to sample the visible state at full
|
||||||
|
* doc resolution before flood-filling. Returns the canvas, or null if a 2D context
|
||||||
|
* can't be obtained.
|
||||||
|
*/
|
||||||
|
_drawingRenderToOffscreen(docW, docH) {
|
||||||
|
const off = document.createElement('canvas');
|
||||||
|
off.width = docW;
|
||||||
|
off.height = docH;
|
||||||
|
const ctx = off.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
|
ctx.fillStyle = CONFIG.DRAWING_BACKGROUND;
|
||||||
|
ctx.fillRect(0, 0, docW, docH);
|
||||||
|
if (this._drawingBaseImage && this._drawingBaseImage.complete) {
|
||||||
|
ctx.drawImage(this._drawingBaseImage, 0, 0, docW, docH);
|
||||||
|
}
|
||||||
|
for (const op of this.drawingOps) {
|
||||||
|
this._drawingDrawOp(ctx, op);
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
},
|
||||||
|
|
||||||
_drawingDisconnectResizeObserver() {
|
_drawingDisconnectResizeObserver() {
|
||||||
if (this._drawingResizeObserver) {
|
if (this._drawingResizeObserver) {
|
||||||
|
|
@ -3205,6 +3235,8 @@ function noteApp() {
|
||||||
const wrap = this.$refs.drawingCanvasWrap;
|
const wrap = this.$refs.drawingCanvasWrap;
|
||||||
if (!canvas || !wrap) return;
|
if (!canvas || !wrap) return;
|
||||||
this._drawingCtx = canvas.getContext('2d');
|
this._drawingCtx = canvas.getContext('2d');
|
||||||
|
this._drawingDisposeOps(this.drawingOps);
|
||||||
|
this._drawingDisposeOps(this.drawingRedoStack);
|
||||||
this.drawingOps = [];
|
this.drawingOps = [];
|
||||||
this.drawingRedoStack = [];
|
this.drawingRedoStack = [];
|
||||||
this.drawingDraft = null;
|
this.drawingDraft = null;
|
||||||
|
|
@ -3272,6 +3304,93 @@ function noteApp() {
|
||||||
return `#${h(r)}${h(g)}${h(b)}`;
|
return `#${h(r)}${h(g)}${h(b)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_drawingHexToRgb(hex) {
|
||||||
|
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || '');
|
||||||
|
if (!m) return { r: 0, g: 0, b: 0 };
|
||||||
|
return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the native bitmap memory backing any 'fill' ops in `ops`. Safe to call on a
|
||||||
|
* mixed list (vector ops are skipped). Caller is responsible for clearing the array
|
||||||
|
* itself afterwards if needed.
|
||||||
|
*/
|
||||||
|
_drawingDisposeOps(ops) {
|
||||||
|
if (!ops) return;
|
||||||
|
for (const op of ops) {
|
||||||
|
if (op && op.type === 'fill' && op.bitmap && typeof op.bitmap.close === 'function') {
|
||||||
|
try { op.bitmap.close(); } catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterative scanline flood fill on an ImageData buffer. Marks every pixel that is
|
||||||
|
* connected to (sx, sy) and matches the seed color exactly (4-way connectivity).
|
||||||
|
* Returns { mask, minX, minY, maxX, maxY } describing the filled region's bounding
|
||||||
|
* box, or null if the seed pixel is already the same color as the fill (so the caller
|
||||||
|
* can skip pushing a no-op onto the undo stack). Stack-based — never recurses, so a
|
||||||
|
* full-canvas fill at 1600×1000 stays well under any browser stack limit.
|
||||||
|
*
|
||||||
|
* Known optimization opportunities (revisit only if fills feel slow in practice):
|
||||||
|
* 1) Add a `tolerance` parameter to soften antialiasing halos around strokes.
|
||||||
|
* 2) Push only the start of each new connected run above/below the current span
|
||||||
|
* instead of every cell — the textbook scanline optimization, ~3–5× faster.
|
||||||
|
* 3) Replace the Array<[x,y]> stack with a flat Int32Array + top pointer to remove
|
||||||
|
* per-push allocations and cut GC pressure on large fills.
|
||||||
|
*/
|
||||||
|
_drawingFlood(imgData, sx, sy, fillRGB) {
|
||||||
|
const { data, width: W, height: H } = imgData;
|
||||||
|
if (sx < 0 || sy < 0 || sx >= W || sy >= H) return null;
|
||||||
|
const seedIdx = (sy * W + sx) * 4;
|
||||||
|
const tR = data[seedIdx];
|
||||||
|
const tG = data[seedIdx + 1];
|
||||||
|
const tB = data[seedIdx + 2];
|
||||||
|
const tA = data[seedIdx + 3];
|
||||||
|
if (tR === fillRGB.r && tG === fillRGB.g && tB === fillRGB.b && tA === 255) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mask = new Uint8Array(W * H);
|
||||||
|
let minX = sx, minY = sy, maxX = sx, maxY = sy;
|
||||||
|
const matches = (px, py) => {
|
||||||
|
if (mask[py * W + px]) return false;
|
||||||
|
const i = (py * W + px) * 4;
|
||||||
|
return (
|
||||||
|
data[i] === tR &&
|
||||||
|
data[i + 1] === tG &&
|
||||||
|
data[i + 2] === tB &&
|
||||||
|
data[i + 3] === tA
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const stack = [[sx, sy]];
|
||||||
|
while (stack.length) {
|
||||||
|
const [x, y] = stack.pop();
|
||||||
|
if (!matches(x, y)) continue;
|
||||||
|
let lx = x;
|
||||||
|
while (lx > 0 && matches(lx - 1, y)) lx--;
|
||||||
|
let rx = x;
|
||||||
|
while (rx < W - 1 && matches(rx + 1, y)) rx++;
|
||||||
|
for (let i = lx; i <= rx; i++) {
|
||||||
|
mask[y * W + i] = 1;
|
||||||
|
}
|
||||||
|
if (lx < minX) minX = lx;
|
||||||
|
if (rx > maxX) maxX = rx;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
if (y > 0) {
|
||||||
|
for (let i = lx; i <= rx; i++) {
|
||||||
|
if (matches(i, y - 1)) stack.push([i, y - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (y < H - 1) {
|
||||||
|
for (let i = lx; i <= rx; i++) {
|
||||||
|
if (matches(i, y + 1)) stack.push([i, y + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { mask, minX, minY, maxX, maxY };
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sample visible canvas color under the pointer; 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
|
* Pointer coords come in document space; we map to device pixels via the canvas's
|
||||||
|
|
@ -3294,6 +3413,75 @@ function noteApp() {
|
||||||
this.drawingColor = this._drawingRgbToHex(pix[0], pix[1], pix[2]);
|
this.drawingColor = this._drawingRgbToHex(pix[0], pix[1], pix[2]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paint-bucket flood fill at the click point. Renders the current state to a fresh
|
||||||
|
* off-screen canvas at document resolution, runs an iterative scanline flood fill from
|
||||||
|
* the clicked pixel, then stores only the cropped delta as a 'fill' op so the existing
|
||||||
|
* undo/redo machinery (which just shuffles ops between drawingOps and drawingRedoStack)
|
||||||
|
* works unchanged. The cropped ImageBitmap keeps memory proportional to the filled
|
||||||
|
* area, not the canvas area.
|
||||||
|
*/
|
||||||
|
async drawingFill(e) {
|
||||||
|
if (this.currentMediaType !== 'drawing' || !this.currentMedia) return;
|
||||||
|
const docW = this._drawingClampDim(this.drawingDocW, CONFIG.DRAWING_DEFAULT_DOC_W);
|
||||||
|
const docH = this._drawingClampDim(this.drawingDocH, CONFIG.DRAWING_DEFAULT_DOC_H);
|
||||||
|
if (!docW || !docH) return;
|
||||||
|
const { x, y } = this._drawingCanvasCoords(e);
|
||||||
|
const sx = Math.floor(x);
|
||||||
|
const sy = Math.floor(y);
|
||||||
|
if (sx < 0 || sy < 0 || sx >= docW || sy >= docH) return;
|
||||||
|
const off = this._drawingRenderToOffscreen(docW, docH);
|
||||||
|
if (!off) return;
|
||||||
|
const offCtx = off.getContext('2d');
|
||||||
|
if (!offCtx) return;
|
||||||
|
const imgData = offCtx.getImageData(0, 0, docW, docH);
|
||||||
|
const fillRGB = this._drawingHexToRgb(this.drawingColor);
|
||||||
|
const result = this._drawingFlood(imgData, sx, sy, fillRGB);
|
||||||
|
// Clicked on a pixel that's already the fill color (or out of bounds): no-op.
|
||||||
|
if (!result) return;
|
||||||
|
const w = result.maxX - result.minX + 1;
|
||||||
|
const h = result.maxY - result.minY + 1;
|
||||||
|
const overlay = new ImageData(w, h);
|
||||||
|
const ovData = overlay.data;
|
||||||
|
const { mask } = result;
|
||||||
|
for (let py = 0; py < h; py++) {
|
||||||
|
const srcRow = (result.minY + py) * docW + result.minX;
|
||||||
|
const dstRow = py * w * 4;
|
||||||
|
for (let px = 0; px < w; px++) {
|
||||||
|
if (mask[srcRow + px]) {
|
||||||
|
const di = dstRow + px * 4;
|
||||||
|
ovData[di] = fillRGB.r;
|
||||||
|
ovData[di + 1] = fillRGB.g;
|
||||||
|
ovData[di + 2] = fillRGB.b;
|
||||||
|
ovData[di + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bitmap;
|
||||||
|
try {
|
||||||
|
bitmap = await createImageBitmap(overlay);
|
||||||
|
} catch (err) {
|
||||||
|
ErrorHandler.handle('drawing fill', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Drawing may have closed (navigation, clear) while createImageBitmap awaited.
|
||||||
|
if (this.currentMediaType !== 'drawing' || !this.currentMedia) {
|
||||||
|
try { bitmap.close(); } catch (_) { /* ignore */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._drawingCancelAutosave();
|
||||||
|
this._drawingDisposeOps(this.drawingRedoStack);
|
||||||
|
this.drawingRedoStack = [];
|
||||||
|
this.drawingOps.push({
|
||||||
|
type: 'fill',
|
||||||
|
x: result.minX,
|
||||||
|
y: result.minY,
|
||||||
|
bitmap,
|
||||||
|
});
|
||||||
|
this.drawingRedraw();
|
||||||
|
this._drawingScheduleAutosave();
|
||||||
|
},
|
||||||
|
|
||||||
/** True when there is a loaded bitmap and/or session strokes to clear away. */
|
/** True when there is a loaded bitmap and/or session strokes to clear away. */
|
||||||
drawingClearEnabled() {
|
drawingClearEnabled() {
|
||||||
if (this.currentMediaType !== 'drawing') return false;
|
if (this.currentMediaType !== 'drawing') return false;
|
||||||
|
|
@ -3327,6 +3515,8 @@ function noteApp() {
|
||||||
this.drawingHasRasterFromFile = false;
|
this.drawingHasRasterFromFile = false;
|
||||||
this.drawingDraft = null;
|
this.drawingDraft = null;
|
||||||
this.drawingIsPointerDown = false;
|
this.drawingIsPointerDown = false;
|
||||||
|
this._drawingDisposeOps(this.drawingOps);
|
||||||
|
this._drawingDisposeOps(this.drawingRedoStack);
|
||||||
this.drawingOps = [];
|
this.drawingOps = [];
|
||||||
this.drawingRedoStack = [];
|
this.drawingRedoStack = [];
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
|
|
@ -3343,11 +3533,17 @@ function noteApp() {
|
||||||
this.drawingSampleColor(e);
|
this.drawingSampleColor(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (tool === 'fill') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.drawingFill(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._drawingCancelAutosave();
|
this._drawingCancelAutosave();
|
||||||
canvas.setPointerCapture(e.pointerId);
|
canvas.setPointerCapture(e.pointerId);
|
||||||
this._drawingPointerId = e.pointerId;
|
this._drawingPointerId = e.pointerId;
|
||||||
const { x, y } = this._drawingCanvasCoords(e);
|
const { x, y } = this._drawingCanvasCoords(e);
|
||||||
this.drawingIsPointerDown = true;
|
this.drawingIsPointerDown = true;
|
||||||
|
this._drawingDisposeOps(this.drawingRedoStack);
|
||||||
this.drawingRedoStack = [];
|
this.drawingRedoStack = [];
|
||||||
const lw = this.drawingLineWidth;
|
const lw = this.drawingLineWidth;
|
||||||
const color = tool === 'eraser' ? CONFIG.DRAWING_BACKGROUND : this.drawingColor;
|
const color = tool === 'eraser' ? CONFIG.DRAWING_BACKGROUND : this.drawingColor;
|
||||||
|
|
@ -3489,19 +3685,8 @@ function noteApp() {
|
||||||
// from a future "resize drawing" feature can never leak between saves.
|
// from a future "resize drawing" feature can never leak between saves.
|
||||||
const docW = this._drawingClampDim(this.drawingDocW, CONFIG.DRAWING_DEFAULT_DOC_W);
|
const docW = this._drawingClampDim(this.drawingDocW, CONFIG.DRAWING_DEFAULT_DOC_W);
|
||||||
const docH = this._drawingClampDim(this.drawingDocH, CONFIG.DRAWING_DEFAULT_DOC_H);
|
const docH = this._drawingClampDim(this.drawingDocH, CONFIG.DRAWING_DEFAULT_DOC_H);
|
||||||
const off = document.createElement('canvas');
|
const off = this._drawingRenderToOffscreen(docW, docH);
|
||||||
off.width = docW;
|
if (!off) return;
|
||||||
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).
|
// Keep the visible canvas in sync (cosmetic; UI doesn't have to wait for the upload).
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
const blob = await new Promise((resolve, reject) => {
|
const blob = await new Promise((resolve, reject) => {
|
||||||
|
|
|
||||||
|
|
@ -2789,6 +2789,19 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 11.25 1.425 1.425c.671.671.671 1.757 0 2.428l-1.06 1.06c-.671.671-1.757.671-2.428 0l-1.425-1.425m6-6 1.425-1.425c.671-.671 1.757-.671 2.428 0l1.06 1.06c.671.671.671 1.757 0 2.428L21 15.75m-6-6L8.25 3.75a2.121 2.121 0 0 0-3 3L12 15.75l3-3m-3-3-6 6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 11.25 1.425 1.425c.671.671.671 1.757 0 2.428l-1.06 1.06c-.671.671-1.757.671-2.428 0l-1.425-1.425m6-6 1.425-1.425c.671-.671 1.757-.671 2.428 0l1.06 1.06c.671.671.671 1.757 0 2.428L21 15.75m-6-6L8.25 3.75a2.121 2.121 0 0 0-3 3L12 15.75l3-3m-3-3-6 6" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
:style="drawingTool === 'fill' ? 'background-color: var(--accent-primary); color: white; border-color: var(--accent-primary);' : 'background-color: var(--bg-tertiary); color: var(--text-secondary); border-color: var(--border-primary);'"
|
||||||
|
@click="drawingTool = 'fill'"
|
||||||
|
:title="t('drawing.tool_fill')"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3l9 9-7.5 7.5a2.121 2.121 0 01-3 0L3 16.5a2.121 2.121 0 010-3L9 3z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.5h15" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 14.5c0 1-1.5 3-1.5 3s-1.5-2-1.5-3a1.5 1.5 0 113 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-2 rounded-lg border transition-colors"
|
class="p-2 rounded-lg border transition-colors"
|
||||||
|
|
@ -3125,7 +3138,9 @@
|
||||||
class="drawing-surface block"
|
class="drawing-surface block"
|
||||||
:style="drawingTool === 'eyedropper'
|
:style="drawingTool === 'eyedropper'
|
||||||
? 'touch-action: none; cursor: copy; background: #fff;'
|
? 'touch-action: none; cursor: copy; background: #fff;'
|
||||||
: 'touch-action: none; cursor: crosshair; background: #fff;'"
|
: drawingTool === 'fill'
|
||||||
|
? 'touch-action: none; cursor: cell; background: #fff;'
|
||||||
|
: 'touch-action: none; cursor: crosshair; background: #fff;'"
|
||||||
@pointerdown.prevent="drawingPointerDown($event)"
|
@pointerdown.prevent="drawingPointerDown($event)"
|
||||||
@pointermove.prevent="drawingPointerMove($event)"
|
@pointermove.prevent="drawingPointerMove($event)"
|
||||||
@pointerup.prevent="drawingPointerUp($event)"
|
@pointerup.prevent="drawingPointerUp($event)"
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
"width": "Strichstärke",
|
"width": "Strichstärke",
|
||||||
"tool_eraser": "Radierer (malt mit Hintergrundfarbe)",
|
"tool_eraser": "Radierer (malt mit Hintergrundfarbe)",
|
||||||
"tool_eyedropper": "Pipette — Farbe vom Bild aufnehmen",
|
"tool_eyedropper": "Pipette — Farbe vom Bild aufnehmen",
|
||||||
|
"tool_fill": "Farbeimer — verbundenen Bereich mit Farbe füllen",
|
||||||
"clear": "Leeren",
|
"clear": "Leeren",
|
||||||
"clear_hint": "Durch leeres Bild ersetzen",
|
"clear_hint": "Durch leeres Bild ersetzen",
|
||||||
"clear_title": "Durch leere Leinwand ersetzen?",
|
"clear_title": "Durch leere Leinwand ersetzen?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "Stroke width",
|
"width": "Stroke width",
|
||||||
"tool_eraser": "Eraser (paints with background colour)",
|
"tool_eraser": "Eraser (paints with background colour)",
|
||||||
"tool_eyedropper": "Eyedropper — pick a colour from the canvas",
|
"tool_eyedropper": "Eyedropper — pick a colour from the canvas",
|
||||||
|
"tool_fill": "Paint bucket — fill connected area with colour",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_hint": "Replace with a blank image",
|
"clear_hint": "Replace with a blank image",
|
||||||
"clear_title": "Replace with a blank canvas?",
|
"clear_title": "Replace with a blank canvas?",
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
"width": "Stroke width",
|
"width": "Stroke width",
|
||||||
"tool_eraser": "Eraser (paints with background color)",
|
"tool_eraser": "Eraser (paints with background color)",
|
||||||
"tool_eyedropper": "Eyedropper — pick a color from the canvas",
|
"tool_eyedropper": "Eyedropper — pick a color from the canvas",
|
||||||
|
"tool_fill": "Paint bucket — fill connected area with color",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_hint": "Replace with a blank image",
|
"clear_hint": "Replace with a blank image",
|
||||||
"clear_title": "Replace with a blank canvas?",
|
"clear_title": "Replace with a blank canvas?",
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
"width": "Grosor del trazo",
|
"width": "Grosor del trazo",
|
||||||
"tool_eraser": "Borrador (pinta con el color de fondo)",
|
"tool_eraser": "Borrador (pinta con el color de fondo)",
|
||||||
"tool_eyedropper": "Cuentagotas — tomar color del lienzo",
|
"tool_eyedropper": "Cuentagotas — tomar color del lienzo",
|
||||||
|
"tool_fill": "Cubo de pintura — rellenar área conectada con color",
|
||||||
"clear": "Borrar",
|
"clear": "Borrar",
|
||||||
"clear_hint": "Sustituir por imagen en blanco",
|
"clear_hint": "Sustituir por imagen en blanco",
|
||||||
"clear_title": "¿Sustituir por lienzo en blanco?",
|
"clear_title": "¿Sustituir por lienzo en blanco?",
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
"width": "Épaisseur du trait",
|
"width": "Épaisseur du trait",
|
||||||
"tool_eraser": "Gomme (peint avec la couleur de fond)",
|
"tool_eraser": "Gomme (peint avec la couleur de fond)",
|
||||||
"tool_eyedropper": "Pipette — choisir une couleur sur le canevas",
|
"tool_eyedropper": "Pipette — choisir une couleur sur le canevas",
|
||||||
|
"tool_fill": "Pot de peinture — remplir la zone connectée avec la couleur",
|
||||||
"clear": "Effacer",
|
"clear": "Effacer",
|
||||||
"clear_hint": "Remplacer par une image vide",
|
"clear_hint": "Remplacer par une image vide",
|
||||||
"clear_title": "Remplacer par un canevas vide ?",
|
"clear_title": "Remplacer par un canevas vide ?",
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
"width": "Vonalvastagság",
|
"width": "Vonalvastagság",
|
||||||
"tool_eraser": "Radír (háttérszínnel fest)",
|
"tool_eraser": "Radír (háttérszínnel fest)",
|
||||||
"tool_eyedropper": "Pipetta — szín mintavétele a vászonról",
|
"tool_eyedropper": "Pipetta — szín mintavétele a vászonról",
|
||||||
|
"tool_fill": "Festékesvödör — összefüggő terület kitöltése színnel",
|
||||||
"clear": "Törlés",
|
"clear": "Törlés",
|
||||||
"clear_hint": "Üres képre cserélés",
|
"clear_hint": "Üres képre cserélés",
|
||||||
"clear_title": "Lecseréled üres vászonra?",
|
"clear_title": "Lecseréled üres vászonra?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "Spessore tratto",
|
"width": "Spessore tratto",
|
||||||
"tool_eraser": "Gomma (disegna col colore di sfondo)",
|
"tool_eraser": "Gomma (disegna col colore di sfondo)",
|
||||||
"tool_eyedropper": "Contagocce — prendi un colore dal canvas",
|
"tool_eyedropper": "Contagocce — prendi un colore dal canvas",
|
||||||
|
"tool_fill": "Secchiello — riempi l'area connessa con il colore",
|
||||||
"clear": "Svuota",
|
"clear": "Svuota",
|
||||||
"clear_hint": "Sostituisci con immagine vuota",
|
"clear_hint": "Sostituisci con immagine vuota",
|
||||||
"clear_title": "Sostituire con tela bianca?",
|
"clear_title": "Sostituire con tela bianca?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "線の太さ",
|
"width": "線の太さ",
|
||||||
"tool_eraser": "消しゴム(背景色で塗る)",
|
"tool_eraser": "消しゴム(背景色で塗る)",
|
||||||
"tool_eyedropper": "スポイト — キャンバスから色を取得",
|
"tool_eyedropper": "スポイト — キャンバスから色を取得",
|
||||||
|
"tool_fill": "塗りつぶし — つながった領域を色で塗りつぶす",
|
||||||
"clear": "クリア",
|
"clear": "クリア",
|
||||||
"clear_hint": "白紙の画像に置き換え",
|
"clear_hint": "白紙の画像に置き換え",
|
||||||
"clear_title": "白紙のキャンバスに置き換えますか?",
|
"clear_title": "白紙のキャンバスに置き換えますか?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "Толщина линии",
|
"width": "Толщина линии",
|
||||||
"tool_eraser": "Ластик (рисует цветом фона)",
|
"tool_eraser": "Ластик (рисует цветом фона)",
|
||||||
"tool_eyedropper": "Пипетка — взять цвет с холста",
|
"tool_eyedropper": "Пипетка — взять цвет с холста",
|
||||||
|
"tool_fill": "Заливка — заполнить связанную область цветом",
|
||||||
"clear": "Очистить",
|
"clear": "Очистить",
|
||||||
"clear_hint": "Заменить пустым изображением",
|
"clear_hint": "Заменить пустым изображением",
|
||||||
"clear_title": "Заменить пустым холстом?",
|
"clear_title": "Заменить пустым холстом?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "Debelina črte",
|
"width": "Debelina črte",
|
||||||
"tool_eraser": "Radirka (riše z barvo ozadja)",
|
"tool_eraser": "Radirka (riše z barvo ozadja)",
|
||||||
"tool_eyedropper": "Kapalka — vzemi barvo s platna",
|
"tool_eyedropper": "Kapalka — vzemi barvo s platna",
|
||||||
|
"tool_fill": "Vedro za barvanje — zapolni povezano območje z barvo",
|
||||||
"clear": "Počisti",
|
"clear": "Počisti",
|
||||||
"clear_hint": "Zamenjaj s prazno sliko",
|
"clear_hint": "Zamenjaj s prazno sliko",
|
||||||
"clear_title": "Zamenjaj s praznim platnom?",
|
"clear_title": "Zamenjaj s praznim platnom?",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
"width": "线条粗细",
|
"width": "线条粗细",
|
||||||
"tool_eraser": "橡皮擦(使用背景色绘制)",
|
"tool_eraser": "橡皮擦(使用背景色绘制)",
|
||||||
"tool_eyedropper": "吸管 — 从画布取色",
|
"tool_eyedropper": "吸管 — 从画布取色",
|
||||||
|
"tool_fill": "油漆桶 — 用颜色填充连通区域",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"clear_hint": "替换为空白图像",
|
"clear_hint": "替换为空白图像",
|
||||||
"clear_title": "替换为空白画布?",
|
"clear_title": "替换为空白画布?",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue