Add AI sketch-to-image generation to folk-drawfast
- Split layout: drawing canvas (left) + AI result (right) - Prompt input with Generate button using /api/image-gen/img2img - Auto-generate toggle: debounced generation after each stroke - Provider selector (fal.ai / Gemini) and strength slider - Loading spinner overlay with shimmer animation - Image preloading before display for smooth transitions - Port descriptors for folk-arrow connections (prompt, sketch, image) - Wider default size (700x520) for split view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de9cc21301
commit
f58445c35e
|
|
@ -7,8 +7,8 @@ const styles = css`
|
||||||
color: var(--rs-text-primary, #1e293b);
|
color: var(--rs-text-primary, #1e293b);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
min-width: 360px;
|
min-width: 520px;
|
||||||
min-height: 420px;
|
min-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
@ -117,11 +117,19 @@ const styles = css`
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.split-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.canvas-area {
|
.canvas-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-area canvas {
|
.canvas-area canvas {
|
||||||
|
|
@ -130,6 +138,163 @@ const styles = css`
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-area {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--rs-bg-muted, #f8fafc);
|
||||||
|
border-left: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-area img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--rs-text-muted, #94a3b8);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-placeholder-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 3px solid #e2e8f0;
|
||||||
|
border-top-color: #f97316;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 2px solid var(--rs-input-border, #e2e8f0);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--rs-input-bg, #fff);
|
||||||
|
color: var(--rs-input-text, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input:focus {
|
||||||
|
border-color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: linear-gradient(135deg, #f97316, #eab308);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--rs-text-muted, #64748b);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle input {
|
||||||
|
accent-color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--rs-bg-surface, white);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-slider {
|
||||||
|
width: 60px;
|
||||||
|
accent-color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--rs-text-muted, #64748b);
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.export-btn {
|
.export-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: #f97316;
|
background: #f97316;
|
||||||
|
|
@ -164,6 +329,12 @@ declare global {
|
||||||
export class FolkDrawfast extends FolkShape {
|
export class FolkDrawfast extends FolkShape {
|
||||||
static override tagName = "folk-drawfast";
|
static override tagName = "folk-drawfast";
|
||||||
|
|
||||||
|
static override portDescriptors = [
|
||||||
|
{ name: "prompt", type: "text" as const, direction: "input" as const },
|
||||||
|
{ name: "sketch", type: "image-url" as const, direction: "output" as const },
|
||||||
|
{ name: "image", type: "image-url" as const, direction: "output" as const },
|
||||||
|
];
|
||||||
|
|
||||||
static {
|
static {
|
||||||
const sheet = new CSSStyleSheet();
|
const sheet = new CSSStyleSheet();
|
||||||
const parentRules = Array.from(FolkShape.styles.cssRules)
|
const parentRules = Array.from(FolkShape.styles.cssRules)
|
||||||
|
|
@ -184,6 +355,15 @@ export class FolkDrawfast extends FolkShape {
|
||||||
#brushSize = 4;
|
#brushSize = 4;
|
||||||
#tool = "pen"; // pen | eraser
|
#tool = "pen"; // pen | eraser
|
||||||
#isDrawing = false;
|
#isDrawing = false;
|
||||||
|
#isGenerating = false;
|
||||||
|
#autoGenerate = false;
|
||||||
|
#autoDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
#provider: "fal" | "gemini" = "fal";
|
||||||
|
#strength = 0.65;
|
||||||
|
#lastResultUrl: string | null = null;
|
||||||
|
#promptInput: HTMLInputElement | null = null;
|
||||||
|
#generateBtn: HTMLButtonElement | null = null;
|
||||||
|
#resultArea: HTMLElement | null = null;
|
||||||
|
|
||||||
get strokes() {
|
get strokes() {
|
||||||
return this.#strokes;
|
return this.#strokes;
|
||||||
|
|
@ -197,7 +377,7 @@ export class FolkDrawfast extends FolkShape {
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
<span>✏️</span>
|
<span>✏️</span>
|
||||||
<span>Drawfast</span>
|
<span>Drawfast AI</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="export-png-btn" title="Export PNG">💾</button>
|
<button class="export-png-btn" title="Export PNG">💾</button>
|
||||||
|
|
@ -216,8 +396,32 @@ export class FolkDrawfast extends FolkShape {
|
||||||
<span style="flex:1"></span>
|
<span style="flex:1"></span>
|
||||||
<button class="tool-btn" data-tool="clear" title="Clear all">🗑️</button>
|
<button class="tool-btn" data-tool="clear" title="Clear all">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas-area">
|
<div class="split-area">
|
||||||
<canvas></canvas>
|
<div class="canvas-area">
|
||||||
|
<canvas></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="result-area">
|
||||||
|
<div class="result-placeholder">
|
||||||
|
<span class="result-placeholder-icon">🎨</span>
|
||||||
|
<span>Draw a sketch and click Generate</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prompt-bar">
|
||||||
|
<input type="text" class="prompt-input" placeholder="Describe what to generate from your sketch..." />
|
||||||
|
<div class="prompt-controls">
|
||||||
|
<label class="auto-toggle" title="Auto-generate after each stroke">
|
||||||
|
<input type="checkbox" class="auto-checkbox" />
|
||||||
|
Auto
|
||||||
|
</label>
|
||||||
|
<select class="provider-select">
|
||||||
|
<option value="fal">fal.ai</option>
|
||||||
|
<option value="gemini">Gemini</option>
|
||||||
|
</select>
|
||||||
|
<input type="range" class="strength-slider" min="30" max="95" value="65" title="AI strength" />
|
||||||
|
<span class="strength-label">65%</span>
|
||||||
|
</div>
|
||||||
|
<button class="generate-btn">Generate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -230,12 +434,19 @@ export class FolkDrawfast extends FolkShape {
|
||||||
|
|
||||||
this.#canvas = wrapper.querySelector(".canvas-area canvas") as HTMLCanvasElement;
|
this.#canvas = wrapper.querySelector(".canvas-area canvas") as HTMLCanvasElement;
|
||||||
this.#ctx = this.#canvas.getContext("2d");
|
this.#ctx = this.#canvas.getContext("2d");
|
||||||
|
this.#promptInput = wrapper.querySelector(".prompt-input") as HTMLInputElement;
|
||||||
|
this.#generateBtn = wrapper.querySelector(".generate-btn") as HTMLButtonElement;
|
||||||
|
this.#resultArea = wrapper.querySelector(".result-area") as HTMLElement;
|
||||||
|
|
||||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||||
const exportBtn = wrapper.querySelector(".export-png-btn") as HTMLButtonElement;
|
const exportBtn = wrapper.querySelector(".export-png-btn") as HTMLButtonElement;
|
||||||
const sizeSlider = wrapper.querySelector(".size-slider") as HTMLInputElement;
|
const sizeSlider = wrapper.querySelector(".size-slider") as HTMLInputElement;
|
||||||
const sizeLabel = wrapper.querySelector(".size-label") as HTMLElement;
|
const sizeLabel = wrapper.querySelector(".size-label") as HTMLElement;
|
||||||
const canvasArea = wrapper.querySelector(".canvas-area") as HTMLElement;
|
const canvasArea = wrapper.querySelector(".canvas-area") as HTMLElement;
|
||||||
|
const autoCheckbox = wrapper.querySelector(".auto-checkbox") as HTMLInputElement;
|
||||||
|
const providerSelect = wrapper.querySelector(".provider-select") as HTMLSelectElement;
|
||||||
|
const strengthSlider = wrapper.querySelector(".strength-slider") as HTMLInputElement;
|
||||||
|
const strengthLabel = wrapper.querySelector(".strength-label") as HTMLElement;
|
||||||
|
|
||||||
// Tool buttons
|
// Tool buttons
|
||||||
wrapper.querySelectorAll(".tool-btn").forEach((btn) => {
|
wrapper.querySelectorAll(".tool-btn").forEach((btn) => {
|
||||||
|
|
@ -260,7 +471,6 @@ export class FolkDrawfast extends FolkShape {
|
||||||
this.#color = (swatch as HTMLElement).dataset.color || "#0f172a";
|
this.#color = (swatch as HTMLElement).dataset.color || "#0f172a";
|
||||||
wrapper.querySelectorAll(".color-swatch").forEach((s) => s.classList.remove("active"));
|
wrapper.querySelectorAll(".color-swatch").forEach((s) => s.classList.remove("active"));
|
||||||
swatch.classList.add("active");
|
swatch.classList.add("active");
|
||||||
// Switch to pen when picking a color
|
|
||||||
this.#tool = "pen";
|
this.#tool = "pen";
|
||||||
wrapper.querySelectorAll(".tool-btn").forEach((b) => b.classList.remove("active"));
|
wrapper.querySelectorAll(".tool-btn").forEach((b) => b.classList.remove("active"));
|
||||||
wrapper.querySelector('[data-tool="pen"]')?.classList.add("active");
|
wrapper.querySelector('[data-tool="pen"]')?.classList.add("active");
|
||||||
|
|
@ -275,6 +485,27 @@ export class FolkDrawfast extends FolkShape {
|
||||||
});
|
});
|
||||||
sizeSlider.addEventListener("pointerdown", (e) => e.stopPropagation());
|
sizeSlider.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
// Strength slider
|
||||||
|
strengthSlider.addEventListener("input", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.#strength = parseInt(strengthSlider.value) / 100;
|
||||||
|
strengthLabel.textContent = strengthSlider.value + "%";
|
||||||
|
});
|
||||||
|
strengthSlider.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
// Auto-generate toggle
|
||||||
|
autoCheckbox.addEventListener("change", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.#autoGenerate = autoCheckbox.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provider select
|
||||||
|
providerSelect.addEventListener("change", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.#provider = providerSelect.value as "fal" | "gemini";
|
||||||
|
});
|
||||||
|
providerSelect.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||||
|
|
||||||
// Drawing events
|
// Drawing events
|
||||||
this.#canvas.addEventListener("pointerdown", (e) => {
|
this.#canvas.addEventListener("pointerdown", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -306,6 +537,10 @@ export class FolkDrawfast extends FolkShape {
|
||||||
this.dispatchEvent(new CustomEvent("stroke-complete", {
|
this.dispatchEvent(new CustomEvent("stroke-complete", {
|
||||||
detail: { stroke: this.#currentStroke },
|
detail: { stroke: this.#currentStroke },
|
||||||
}));
|
}));
|
||||||
|
// Auto-generate on stroke complete if enabled
|
||||||
|
if (this.#autoGenerate && this.#promptInput?.value.trim()) {
|
||||||
|
this.#scheduleAutoGenerate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.#currentStroke = null;
|
this.#currentStroke = null;
|
||||||
};
|
};
|
||||||
|
|
@ -313,6 +548,21 @@ export class FolkDrawfast extends FolkShape {
|
||||||
this.#canvas.addEventListener("pointerup", endDraw);
|
this.#canvas.addEventListener("pointerup", endDraw);
|
||||||
this.#canvas.addEventListener("pointerleave", endDraw);
|
this.#canvas.addEventListener("pointerleave", endDraw);
|
||||||
|
|
||||||
|
// Generate button
|
||||||
|
this.#generateBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.#generate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key in prompt
|
||||||
|
this.#promptInput.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.#generate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#promptInput.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
exportBtn.addEventListener("click", (e) => {
|
exportBtn.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -340,6 +590,99 @@ export class FolkDrawfast extends FolkShape {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#scheduleAutoGenerate() {
|
||||||
|
if (this.#autoDebounceTimer) clearTimeout(this.#autoDebounceTimer);
|
||||||
|
this.#autoDebounceTimer = setTimeout(() => {
|
||||||
|
this.#autoDebounceTimer = null;
|
||||||
|
if (!this.#isGenerating) {
|
||||||
|
this.#generate();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #generate() {
|
||||||
|
const prompt = this.#promptInput?.value.trim();
|
||||||
|
if (!prompt || this.#isGenerating || !this.#canvas) return;
|
||||||
|
|
||||||
|
this.#isGenerating = true;
|
||||||
|
if (this.#generateBtn) this.#generateBtn.disabled = true;
|
||||||
|
this.#renderLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceImage = this.#canvas.toDataURL("image/jpeg", 0.8);
|
||||||
|
|
||||||
|
const response = await fetch("/api/image-gen/img2img", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt,
|
||||||
|
source_image: sourceImage,
|
||||||
|
provider: this.#provider,
|
||||||
|
strength: this.#strength,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Generation failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
const imageUrl = result.url || result.image_url;
|
||||||
|
if (!imageUrl) throw new Error("No image returned");
|
||||||
|
|
||||||
|
// Preload image before displaying
|
||||||
|
await this.#preloadImage(imageUrl);
|
||||||
|
|
||||||
|
this.#lastResultUrl = imageUrl;
|
||||||
|
this.#renderResult(imageUrl, prompt);
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("image-generated", {
|
||||||
|
detail: { url: imageUrl, prompt },
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : "Generation failed";
|
||||||
|
this.#renderError(msg);
|
||||||
|
} finally {
|
||||||
|
this.#isGenerating = false;
|
||||||
|
if (this.#generateBtn) this.#generateBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#preloadImage(url: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => resolve();
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderLoading() {
|
||||||
|
if (!this.#resultArea) return;
|
||||||
|
this.#resultArea.innerHTML = `
|
||||||
|
<div class="spinner-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span class="spinner-text">Generating...</span>
|
||||||
|
</div>
|
||||||
|
${this.#lastResultUrl ? `<img src="${this.#escapeAttr(this.#lastResultUrl)}" alt="Previous result" style="opacity:0.3" />` : ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderResult(url: string, prompt: string) {
|
||||||
|
if (!this.#resultArea) return;
|
||||||
|
this.#resultArea.innerHTML = `<img src="${this.#escapeAttr(url)}" alt="${this.#escapeAttr(prompt)}" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderError(msg: string) {
|
||||||
|
if (!this.#resultArea) return;
|
||||||
|
this.#resultArea.innerHTML = `
|
||||||
|
<div class="result-placeholder">
|
||||||
|
<span class="result-placeholder-icon">⚠️</span>
|
||||||
|
<span style="color:#ef4444">${this.#escapeHtml(msg)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
#resizeCanvas(container: HTMLElement) {
|
#resizeCanvas(container: HTMLElement) {
|
||||||
if (!this.#canvas) return;
|
if (!this.#canvas) return;
|
||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
@ -370,7 +713,6 @@ export class FolkDrawfast extends FolkShape {
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = stroke.color;
|
ctx.strokeStyle = stroke.color;
|
||||||
|
|
||||||
// Draw last segment for live drawing
|
|
||||||
const len = stroke.points.length;
|
const len = stroke.points.length;
|
||||||
const p0 = stroke.points[len - 2];
|
const p0 = stroke.points[len - 2];
|
||||||
const p1 = stroke.points[len - 1];
|
const p1 = stroke.points[len - 1];
|
||||||
|
|
@ -400,7 +742,6 @@ export class FolkDrawfast extends FolkShape {
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
// Fill white background
|
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
|
|
@ -442,6 +783,16 @@ export class FolkDrawfast extends FolkShape {
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#escapeHtml(text: string): string {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
#escapeAttr(text: string): string {
|
||||||
|
return text.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
static override fromData(data: Record<string, any>): FolkDrawfast {
|
static override fromData(data: Record<string, any>): FolkDrawfast {
|
||||||
const shape = FolkShape.fromData(data) as FolkDrawfast;
|
const shape = FolkShape.fromData(data) as FolkDrawfast;
|
||||||
return shape;
|
return shape;
|
||||||
|
|
@ -457,10 +808,15 @@ export class FolkDrawfast extends FolkShape {
|
||||||
size: s.size,
|
size: s.size,
|
||||||
tool: s.tool,
|
tool: s.tool,
|
||||||
})),
|
})),
|
||||||
|
lastResultUrl: this.#lastResultUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override applyData(data: Record<string, any>): void {
|
override applyData(data: Record<string, any>): void {
|
||||||
super.applyData(data);
|
super.applyData(data);
|
||||||
|
if (data.lastResultUrl && this.#resultArea) {
|
||||||
|
this.#lastResultUrl = data.lastResultUrl;
|
||||||
|
this.#renderResult(data.lastResultUrl, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4071,7 +4071,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
||||||
"folk-multisig-email": { width: 400, height: 380 },
|
"folk-multisig-email": { width: 400, height: 380 },
|
||||||
"folk-splat": { width: 480, height: 420 },
|
"folk-splat": { width: 480, height: 420 },
|
||||||
"folk-blender": { width: 420, height: 520 },
|
"folk-blender": { width: 420, height: 520 },
|
||||||
"folk-drawfast": { width: 500, height: 480 },
|
"folk-drawfast": { width: 700, height: 520 },
|
||||||
"folk-freecad": { width: 400, height: 480 },
|
"folk-freecad": { width: 400, height: 480 },
|
||||||
"folk-kicad": { width: 420, height: 500 },
|
"folk-kicad": { width: 420, height: 500 },
|
||||||
"folk-canvas": { width: 600, height: 400 },
|
"folk-canvas": { width: 600, height: 400 },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue