242 lines
9.2 KiB
JavaScript
242 lines
9.2 KiB
JavaScript
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);
|
||
});
|
||
}); |