feat(imggen): фон питомца, обложки курсов, аватары и доска через ИИ
Питомец: кастомный фон (миграция 068 pet_bg_custom, POST /api/pet/bg/custom, карточка «Свой фон (ИИ)» в гардеробной, применение картинкой). Курсы: обложка-картинка (миграция 069 cover_image, генерация в модалке редактирования, рендер вместо эмодзи). Аватар: кнопка «Сгенерировать (ИИ)» в загрузке → кадрирование → модерация. Доска (classroom): кнопка-инструмент «Сгенерировать картинку (ИИ)». Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+48
-27
@@ -2452,6 +2452,7 @@
|
||||
<button class="cr-tool-btn" id="wb-tool-text" onclick="wbSetTool('text')" title="Текст"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg></button>
|
||||
<button class="cr-tool-btn" id="wb-tool-mindmap" onclick="wbSetTool('mindmap')" title="Интеллект-карта"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><line x1="12" y1="9" x2="12" y2="3"/><circle cx="12" cy="3" r="1.5"/><line x1="14.6" y1="13.4" x2="19" y2="17"/><circle cx="20" cy="18" r="1.5"/><line x1="9.4" y1="13.4" x2="5" y2="17"/><circle cx="4" cy="18" r="1.5"/><line x1="15" y1="11" x2="21" y2="9"/><circle cx="22" cy="8.5" r="1.5"/><line x1="9" y1="11" x2="3" y2="9"/><circle cx="2" cy="8.5" r="1.5"/></svg></button>
|
||||
<button class="cr-tool-btn" id="wb-tool-image" onclick="wbOpenImagePicker()" title="Изображение"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></button>
|
||||
<button class="cr-tool-btn" id="wb-tool-image-ai" onclick="wbGenerateImage()" title="Сгенерировать картинку (ИИ)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3l2.2 6.3L22 12l-6.8 2.7L13 21l-2.2-6.3L4 12l6.8-2.7z"/><path d="M5 4v3M3.5 5.5h3"/></svg></button>
|
||||
<input type="file" id="wb-image-input" accept="image/*" style="display:none" onchange="wbImageSelected(this)">
|
||||
<div class="cr-tool-sep"></div>
|
||||
<!-- undo / redo / clear -->
|
||||
@@ -3093,6 +3094,7 @@
|
||||
<script src="/js/board-clip.js"></script>
|
||||
<script src="/js/classroom-rtc.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/imggen.js"></script>
|
||||
<script src="/js/sound.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/notifications.js"></script>
|
||||
@@ -6557,45 +6559,64 @@
|
||||
document.getElementById('wb-image-input')?.click();
|
||||
}
|
||||
|
||||
// Поместить загруженный <img> на доску (ресайз до 800px, по центру)
|
||||
function wbPlaceImageFromImg(img) {
|
||||
if (!_wb || !_sessionId) return;
|
||||
const maxPx = 800;
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > maxPx || h > maxPx) {
|
||||
if (w >= h) { h = Math.round(h * maxPx / w); w = maxPx; }
|
||||
else { w = Math.round(w * maxPx / h); h = maxPx; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
const src = canvas.toDataURL('image/jpeg', 0.8);
|
||||
const vw = (w / (img.naturalWidth || w)) * 800;
|
||||
const vh = (h / (img.naturalHeight || h)) * 450;
|
||||
const vx = (1920 - vw) / 2;
|
||||
const vy = (1080 - vh) / 2;
|
||||
const stroke = {
|
||||
id: _wb._localIdCounter--,
|
||||
tool: 'image',
|
||||
data: { src, x: vx, y: vy, w: vw, h: vh },
|
||||
};
|
||||
_wb._strokes.push(stroke);
|
||||
_wb._undoStack.push(stroke.id);
|
||||
_wb.render();
|
||||
if (_wb._onStrokeDone) _wb._onStrokeDone(stroke);
|
||||
}
|
||||
|
||||
function wbImageSelected(input) {
|
||||
const file = input.files?.[0];
|
||||
if (!file || !_wb || !_sessionId) return;
|
||||
input.value = '';
|
||||
const maxPx = 800;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// Resize to max 800px on longest side
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > maxPx || h > maxPx) {
|
||||
if (w >= h) { h = Math.round(h * maxPx / w); w = maxPx; }
|
||||
else { w = Math.round(w * maxPx / h); h = maxPx; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
const src = canvas.toDataURL('image/jpeg', 0.8);
|
||||
// Place image centered on whiteboard in virtual coords
|
||||
const vw = (w / (img.naturalWidth || w)) * 800;
|
||||
const vh = (h / (img.naturalHeight || h)) * 450;
|
||||
const vx = (1920 - vw) / 2;
|
||||
const vy = (1080 - vh) / 2;
|
||||
const stroke = {
|
||||
id: _wb._localIdCounter--,
|
||||
tool: 'image',
|
||||
data: { src, x: vx, y: vy, w: vw, h: vh },
|
||||
};
|
||||
_wb._strokes.push(stroke);
|
||||
_wb._undoStack.push(stroke.id);
|
||||
_wb.render();
|
||||
if (_wb._onStrokeDone) _wb._onStrokeDone(stroke);
|
||||
};
|
||||
img.onload = () => wbPlaceImageFromImg(img);
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Сгенерировать картинку ИИ и вставить на доску
|
||||
function wbGenerateImage() {
|
||||
if (!_wb || !_sessionId) return;
|
||||
if (!LS.imagePromptModal) { LS.toast?.('Модуль генерации не загружен'); return; }
|
||||
LS.imagePromptModal({
|
||||
title: 'Картинка на доску (ИИ)',
|
||||
placeholder: 'Опиши иллюстрацию: «схема круговорота воды, плоский стиль»',
|
||||
useLabel: 'Вставить на доску',
|
||||
onUse: (url) => {
|
||||
const img = new Image();
|
||||
img.onload = () => wbPlaceImageFromImg(img);
|
||||
img.onerror = () => LS.toast?.('Не удалось загрузить картинку', 'error');
|
||||
img.src = url;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function wbSetCustomColor(input) {
|
||||
if (!_wb) return;
|
||||
_wb.setColor(input.value);
|
||||
|
||||
Reference in New Issue
Block a user