async function imageWithTextBlob(imageUrl, text, options = {}) { const response = await fetch(imageUrl, { mode: 'cors', credentials: 'same-origin', }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const imageBlob = await response.blob(); const imageBitmap = await createImageBitmap(imageBlob); // OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed) const canvas = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas( options.width ?? imageBitmap.width, options.height ?? imageBitmap.height ) : document.createElement('canvas'); const width = canvas.width ?? (options.width ?? imageBitmap.width); const height = canvas.height ?? (options.height ?? imageBitmap.height); if (!(canvas instanceof OffscreenCanvas)) { canvas.width = width; canvas.height = height; } const ctx = canvas.getContext('2d', { alpha: true }); // Draw image ctx.drawImage(imageBitmap, 0, 0, width, height); // No text: return original bitmap as blob (rendered) if (!text?.trim()) { if (canvas.convertToBlob) { return canvas.convertToBlob({ type: options.type ?? 'image/png', quality: options.quality ?? 0.92, }); } return await new Promise((resolve) => canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) ); } // === Text rendering === const basePadding = 6; const fontSize = options.fontSize ?? Math.round(width * 0.05); ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`; ctx.textBaseline = 'bottom'; ctx.textAlign = 'center'; const textColor = options.textColor ?? '#5F1798'; const bgColor = options.bgColor ?? '#fff'; const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)'; const shadowBlur = options.shadowBlur ?? 6; const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0); // Wrap text into lines const words = text.split(' '); let line = ''; const lines = []; for (const word of words) { const testLine = line ? line + ' ' + word : word; if (ctx.measureText(testLine).width > maxWidth && line) { lines.push(line); line = word; } else { line = testLine; } } if (line) lines.push(line); const lineHeight = fontSize + (options.lineGap ?? 6); const boxHeight = lines.length * lineHeight + basePadding * 2; const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width)); const boxWidth = maxLineWidth + basePadding * 2; const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.6); const boxX = (width - boxWidth) / 2; const boxY = height - bottomPadding - boxHeight; const radius = options.radius ?? 8; // Background rounded rect ctx.save(); ctx.fillStyle = bgColor; ctx.shadowColor = strokeColor; ctx.shadowBlur = shadowBlur; roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false); ctx.restore(); // Text ctx.fillStyle = textColor; ctx.shadowColor = 'transparent'; for (let i = 0; i < lines.length; i++) { const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2; const lineX = width / 2; if (options.outline) { ctx.lineWidth = options.outlineWidth ?? 2; ctx.strokeStyle = options.outlineColor ?? 'rgba(255,255,255,0.8)'; ctx.strokeText(lines[i], lineX, lineY); } ctx.fillText(lines[i], lineX, lineY); } // Convert to blob if (canvas.convertToBlob) { return await canvas.convertToBlob({ type: options.type ?? 'image/png', quality: options.quality ?? 0.92, }); } return await new Promise((resolve) => canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) ); } function roundRect(ctx, x, y, w, h, r, fill, stroke) { if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r }; ctx.beginPath(); ctx.moveTo(x + r.tl, y); ctx.lineTo(x + w - r.tr, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr); ctx.lineTo(x + w, y + h - r.br); ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h); ctx.lineTo(x + r.bl, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl); ctx.lineTo(x, y + r.tl); ctx.quadraticCurveTo(x, y, x + r.tl, y); ctx.closePath(); if (fill) ctx.fill(); if (stroke) ctx.stroke(); } document.addEventListener('DOMContentLoaded', () => { const shareModal = document.getElementById('shareModal'); const closeshareModal = document.getElementById('closeModal'); const fb = document.getElementById('fb'); const vk = document.getElementById('vk'); const tg = document.getElementById('tg'); const ok = document.getElementById('ok'); const wa = document.getElementById('wa'); const inst = document.getElementById('inst'); const copyBtn = document.getElementById('copyBtn'); const copyMsg = document.getElementById('copyMsg'); function buildShareUrl(platform, url, text = '') { const u = encodeURIComponent(url); const t = encodeURIComponent(text); switch (platform) { case 'tw': return `https://twitter.com/intent/tweet?url=${u}&text=${t}`; case 'vk': return `https://vk.com/share.php?url=${u}&title=${t}`; case 'tg': return `https://telegram.me/share/url?url=${u}&text=${t}`; case 'fb': return `https://www.facebook.com/sharer/sharer.php?u=${u}"e=${t}`; case 'ok': return `https://connect.ok.ru/offer?url=${u}&title=${t}`; case 'wa': return `https://api.whatsapp.com/send?text=${t} ${u}`; case 'inst': return `https://www.instagram.com/?url=${u}`; default: return '#'; } } const shareBtns = document.querySelectorAll('#shareBtn'); shareBtns.forEach(btn => { btn.addEventListener('click', async () => { const link = window.location.origin + btn.dataset.link || window.location.href; const text = btn.dataset.text || document.title; const image = btn.dataset.image; const imagetext = btn.dataset.imagetext || ''; let blob = null; let file = null; let objectUrl = null; try { if (image) { const imageUrl = window.location.origin + image; // В обработчике кнопки const blob = await imageWithTextBlob(imageUrl, imagetext, { textColor: '#5F1798', }); console.log(`Generated blob for "${imagetext}" | size: ${blob.size} | url: ${imageUrl}`); const filename = `shared-image-${Date.now()}.png`; file = new File([blob], filename, { type: blob.type }); // Создаём object URL только если хотим (необязательно для navigator.share) objectUrl = URL.createObjectURL(file); if (navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ title: text, text: text, url: link, files: [file], }); } else { await navigator.share({ title: text, text: '', url: link, }); } } else { await navigator.share({ title: text, text: '', url: link }); } } catch (err) { openshareModal(link, text); } finally { // Очистка try { if (objectUrl) { URL.revokeObjectURL(objectUrl); objectUrl = null; } } catch (e) { /* ignore */ } // Удаляем ссылки, чтобы GC мог собрать объекты blob = null; file = null; } }); }); function openshareModal(link, text) { vk.href = buildShareUrl('vk', link, text); tg.href = buildShareUrl('tg', link, text); fb.href = buildShareUrl('fb', link, text); ok.href = buildShareUrl('ok', link, text); wa.href = buildShareUrl('wa', link, text); inst.href = buildShareUrl('inst', link, text); shareModal.style.display = 'flex'; copyBtn.dataset.link = link; shareModal.style.display = 'flex' } closeshareModal.addEventListener('click', () => { shareModal.style.display = 'none'; }); shareModal.addEventListener('click', (e) => { if (e.target === shareModal) shareModal.style.display = 'none'; }); async function copyToClipboard(text) { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } showCopyMessage(); } catch (e) { showToast({message: 'Не удалось скопировать ссылку', status: 'error'}); } } function showCopyMessage() { copyMsg.style.display = 'inline'; setTimeout(() => copyMsg.style.display = 'none', 2000); } copyBtn.addEventListener('click', () => { const link = copyBtn.dataset.link || window.location.href; copyToClipboard(link); }); });