Drawn Picture — онлайн-рисовалка анимации
Drawn Picture — онлайн-рисовалка
Плагин добавляет на любую страницу или запись сайта настоящую рисовалку. Всё, что нужно сделать — вставить в текст страницы короткое слово-команду, и на этом месте появится холст, на котором сможет рисовать любой посетитель прямо в браузере. Ничего скачивать и устанавливать ему не придётся.
Что видно на странице
Сверху идёт панель настроек. Там можно выбрать цвет и готовые оттенки в палитре, настроить толщину линии, указать размер текста, поменять цвет фона. Отдельные кнопки позволяют отменить последнее действие, вернуть его обратно или очистить холст целиком. Тут же кнопки для сохранения рисунка и получения готового результата.
Ниже расположен ряд инструментов: кисть, ластик, фигуры, текст. Под ними — сам холст. Он выглядит как шахматная доска — так показывают, что фон прозрачный, если его не перекрашивать.
Что можно рисовать
Обычной кистью — что угодно от руки: линии, круги, каракули, буквы. Толщину и цвет выбираешь сам. Ластиком стираешь лишнее, причём тоже с настраиваемой толщиной.
Если переключиться на фигуры, можно тянуть мышкой от угла к углу — и фигура будет расти вместе с курсором. Доступны линия, прямоугольник, круг, треугольник, звезда, стрелка и сердечко. Есть отдельная кнопка «Заливка»: если её включить, фигура закрашивается цветом целиком, а если выключить — остаётся только контур.
Текстом можно подписать рисунок. Кликаешь по нужному месту — появляется небольшое поле ввода. Пишешь, что хочешь, нажимаешь Enter — надпись встаёт на холст. Хочешь длинную подпись с переносом строки — просто нажимаешь Shift+Enter. Если передумал — Esc, и текст не появится. Размер шрифта настраивается заранее в панели.
Что можно делать с готовым рисунком
Первое — скачать картинкой. Нажимаешь кнопку, и браузер сохраняет то, что получилось, в виде обычной картинки на компьютер.
Второе — получить готовый кусочек для вставки на другой сайт. Откроется окно с кодом, который можно скопировать и вставить у себя — например, в блок «Произвольный HTML» на WordPress, Tilda или в любом другом конструкторе. На новом месте появится точно такая же рисовалка, только уже без панели управления — просто рисунок, который ты нарисовал.
Третье — открыть рисунок в отдельном окне браузера, чтобы показать кому-то или сохранить как картинку без лишних кнопок и панелей.
Как всё устроено
Один раз вставил команду в страницу — рисовалка появляется в нужном месте и работает сама. Никаких настроек на сервере, никаких баз данных, ничего никуда не отправляется. Всё происходит прямо в браузере посетителя.
Если посетитель откроет страницу с телефона, рисовалка тоже работает — просто холст становится меньше и удобнее для пальца. Если он сузит окно браузера или повернёт телефон, холст сам подстроится под новый размер, сохранив то, что уже нарисовано.
Пока открыто поле для ввода текста, холст не реагирует на клики — так ты случайно не поставишь лишний штрих рядом с подписью.
Кому это пригодится
Если ведёшь сайт про творчество, рисование, детей, обучение — рисовалка отлично подойдёт как интерактив для читателей. Можно предложить посетителям нарисовать что-то на заданную тему, провести мини-конкурс, показать пример и дать попробовать самим. Или просто поставить на странице «связь со мной» и предложить оставить зарисовку.
Всё, что нужно — один раз вставить команду в запись, и рисовалка готова принимать посетителей.
<script>
(function () {
'use strict';
console.log('[DrawnPicture] Скрипт загружен');
function initApp(root) {
if (!root) return;
if (root.dataset.dpInit === '1') return;
root.dataset.dpInit = '1';
root.innerHTML = '';
console.log('[DrawnPicture] Инициализация контейнера', root);
var HEIGHT = parseInt(root.dataset.height || '520', 10);
var toolbar = document.createElement('div'); toolbar.className = 'dp-toolbar';
var toolRow = document.createElement('div'); toolRow.className = 'dp-tool-row';
var shapeRow = document.createElement('div'); shapeRow.className = 'dp-tool-row';
shapeRow.style.display = 'none';
var textRow = document.createElement('div'); textRow.className = 'dp-tool-row';
textRow.style.display = 'none';
var stage = document.createElement('div'); stage.className = 'dp-stage';
stage.style.height = HEIGHT + 'px';
var canvas = document.createElement('canvas');
stage.appendChild(canvas);
var status = document.createElement('div'); status.className = 'dp-status';
var hint = document.createElement('div'); hint.className = 'dp-hint';
hint.textContent = 'Инструменты: кисть, ластик, фигуры, текст. Кнопки экспорта — в панели.';
root.appendChild(toolbar);
root.appendChild(toolRow);
root.appendChild(shapeRow);
root.appendChild(textRow);
root.appendChild(stage);
root.appendChild(status);
root.appendChild(hint);
var ctx = canvas.getContext('2d');
var W = 0, H = 0, dpr = Math.min(2, window.devicePixelRatio || 1);
function resize() {
W = stage.clientWidth;
H = stage.clientHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
redraw();
}
window.addEventListener('resize', resize);
var items = [];
var redo = [];
var drawing = false;
var current = null;
var dragStart = null;
var color = '#2b6cff';
var brush = 4;
var bgFill = 'transparent';
var tool = 'brush';
var shape = 'rect';
var shapeFill = false;
var fontSize = 24;
function toNorm(x, y) { return [x / W, y / H]; }
function fromNorm(p) { return [p[0] * W, p[1] * H]; }
function drawItem(it) {
if (it.type === 'stroke') return drawStroke(it);
if (it.type === 'shape') return drawShape(it);
if (it.type === 'text') return drawText(it);
}
function drawStroke(s) {
var pts = s.p;
if (!pts.length) return;
ctx.save();
ctx.lineWidth = s.w;
if (s.m === 1) {
ctx.globalCompositeOperation = 'destination-out';
ctx.strokeStyle = 'rgba(0,0,0,1)';
} else {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = s.c;
}
if (pts.length === 1) {
var p = fromNorm(pts[0]);
ctx.beginPath();
ctx.arc(p[0], p[1], s.w / 2, 0, Math.PI * 2);
ctx.fillStyle = ctx.strokeStyle;
ctx.fill();
} else {
ctx.beginPath();
var f = fromNorm(pts[0]);
ctx.moveTo(f[0], f[1]);
for (var i = 1; i < pts.length; i++) {
var q = fromNorm(pts[i]);
ctx.lineTo(q[0], q[1]);
}
ctx.stroke();
}
ctx.restore();
}
function drawShape(s) {
var a = fromNorm(s.p[0]), b = fromNorm(s.p[1]);
var x0 = Math.min(a[0], b[0]), y0 = Math.min(a[1], b[1]);
var x1 = Math.max(a[0], b[0]), y1 = Math.max(a[1], b[1]);
var w = x1 - x0, h = y1 - y0;
ctx.save();
ctx.lineWidth = s.w;
ctx.strokeStyle = s.c;
ctx.fillStyle = s.c;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
if (s.shape === 'line') {
ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
} else if (s.shape === 'rect') {
if (s.fill) ctx.fillRect(x0, y0, w, h); else ctx.strokeRect(x0, y0, w, h);
} else if (s.shape === 'circle') {
ctx.beginPath();
ctx.ellipse((x0+x1)/2, (y0+y1)/2, w/2, h/2, 0, 0, Math.PI * 2);
if (s.fill) ctx.fill(); else ctx.stroke();
} else if (s.shape === 'triangle') {
ctx.beginPath();
ctx.moveTo((x0+x1)/2, y0); ctx.lineTo(x1, y1); ctx.lineTo(x0, y1);
ctx.closePath();
if (s.fill) ctx.fill(); else ctx.stroke();
} else if (s.shape === 'star') {
drawStar(ctx, (x0+x1)/2, (y0+y1)/2, 5, Math.min(w,h)/2, Math.min(w,h)/4, s.fill);
} else if (s.shape === 'arrow') {
var angle = Math.atan2(b[1]-a[1], b[0]-a[0]);
var headLen = Math.max(10, s.w * 4);
ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
ctx.beginPath();
ctx.moveTo(b[0], b[1]);
ctx.lineTo(b[0] - headLen*Math.cos(angle - Math.PI/6), b[1] - headLen*Math.sin(angle - Math.PI/6));
ctx.lineTo(b[0] - headLen*Math.cos(angle + Math.PI/6), b[1] - headLen*Math.sin(angle + Math.PI/6));
ctx.closePath(); ctx.fill();
} else if (s.shape === 'heart') {
var hx = (x0+x1)/2, hy = (y0+y1)/2, hw = w/2, hh = h/2;
ctx.beginPath();
ctx.moveTo(hx, hy + hh);
ctx.bezierCurveTo(hx - hw*2, hy - hh*0.2, hx - hw*0.6, hy - hh*1.6, hx, hy - hh*0.6);
ctx.bezierCurveTo(hx + hw*0.6, hy - hh*1.6, hx + hw*2, hy - hh*0.2, hx, hy + hh);
ctx.closePath();
if (s.fill) ctx.fill(); else ctx.stroke();
}
ctx.restore();
}
function drawStar(c, cx, cy, spikes, outer, inner, fill) {
var rot = -Math.PI / 2, step = Math.PI / spikes;
c.beginPath();
c.moveTo(cx + Math.cos(rot) * outer, cy + Math.sin(rot) * outer);
for (var i = 0; i < spikes; i++) {
rot += step; c.lineTo(cx + Math.cos(rot) * inner, cy + Math.sin(rot) * inner);
rot += step; c.lineTo(cx + Math.cos(rot) * outer, cy + Math.sin(rot) * outer);
}
c.closePath();
if (fill) c.fill(); else c.stroke();
}
function drawText(t) {
var p = fromNorm(t.p);
ctx.save();
ctx.fillStyle = t.c;
ctx.font = 'bold ' + t.size + 'px -apple-system, "Segoe UI", Roboto, Arial, sans-serif';
ctx.textBaseline = 'top';
var lines = String(t.text).split('\n');
for (var i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], p[0], p[1] + i * t.size * 1.2);
}
ctx.restore();
}
function redraw() {
ctx.clearRect(0, 0, W, H);
if (bgFill !== 'transparent') {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = bgFill;
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
for (var i = 0; i < items.length; i++) drawItem(items[i]);
if (dragStart && current && current.type === 'shape') {
drawShape({ shape: shape, c: color, w: brush, fill: shapeFill, p: [dragStart, current.p[1]] });
}
}
function pos(e) {
var r = canvas.getBoundingClientRect();
if (e.touches && e.touches.length) {
return [e.touches[0].clientX - r.left, e.touches[0].clientY - r.top];
}
return [e.clientX - r.left, e.clientY - r.top];
}
function begin(x, y) {
if (stage.querySelector('.dp-text-inline')) return;
var p = toNorm(x, y);
drawing = true;
if (tool === 'brush' || tool === 'erase') {
current = { type: 'stroke', c: color, w: brush, m: tool === 'erase' ? 1 : 0, p: [p] };
items.push(current);
redo = [];
drawStroke(current);
} else if (tool === 'shape') {
dragStart = p;
current = { type: 'shape', shape: shape, c: color, w: brush, fill: shapeFill, p: [p, p] };
} else if (tool === 'text') {
openTextInput(x, y);
drawing = false;
}
}
function extend(x, y) {
if (!drawing) return;
var p = toNorm(x, y);
if (current && current.type === 'stroke') {
var last = current.p[current.p.length - 1];
var dx = (p[0]-last[0]) * W, dy = (p[1]-last[1]) * H;
if (dx*dx + dy*dy < 3) return;
current.p.push(p);
drawStroke(current);
} else if (current && current.type === 'shape') {
current.p[1] = p;
redraw();
}
}
function end() {
if (drawing && current && current.type === 'shape') {
items.push(current);
redo = [];
}
drawing = false;
current = null;
dragStart = null;
redraw();
}
canvas.addEventListener('mousedown', function (e) {
if (e.button !== 0) return;
if (stage.querySelector('.dp-text-inline')) return;
var p = pos(e);
begin(p[0], p[1]);
});
canvas.addEventListener('mousemove', function (e) {
if (stage.querySelector('.dp-text-inline')) return;
var p = pos(e);
extend(p[0], p[1]);
});
window.addEventListener('mouseup', end);
canvas.addEventListener('touchstart', function (e) {
if (stage.querySelector('.dp-text-inline')) return;
e.preventDefault();
var p = pos(e);
begin(p[0], p[1]);
}, { passive: false });
canvas.addEventListener('touchmove', function (e) {
if (stage.querySelector('.dp-text-inline')) return;
e.preventDefault();
var p = pos(e);
extend(p[0], p[1]);
}, { passive: false });
window.addEventListener('touchend', end);
window.addEventListener('touchcancel', end);
function openTextInput(x, y) {
var existing = stage.querySelector('.dp-text-inline');
if (existing) existing.blur();
var inp = document.createElement('textarea');
inp.className = 'dp-text-inline';
inp.placeholder = 'Введите текст (Shift+Enter — новая строка)';
inp.rows = 2;
inp.style.left = x + 'px';
inp.style.top = y + 'px';
inp.style.color = color;
inp.style.fontSize = fontSize + 'px';
stage.appendChild(inp);
setTimeout(function () { inp.focus(); }, 0);
var committed = false;
function commit(save) {
if (committed) return;
committed = true;
if (save) {
var val = inp.value;
if (val.trim()) {
items.push({ type: 'text', c: color, size: fontSize, text: val, p: toNorm(x, y) });
redo = [];
redraw();
setStatus('Текст добавлен.');
}
}
if (inp.parentNode) inp.parentNode.removeChild(inp);
}
inp.addEventListener('blur', function () { commit(true); });
inp.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); commit(true); }
if (e.key === 'Escape') { e.preventDefault(); commit(false); }
});
inp.addEventListener('mousedown', function (e) { e.stopPropagation(); });
inp.addEventListener('touchstart', function (e) { e.stopPropagation(); });
}
function group() { var g = document.createElement('div'); g.className = 'dp-group'; return g; }
function mkBtn(text, cls, handler) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'dp-btn' + (cls ? ' ' + cls : '');
b.textContent = text;
b.addEventListener('click', handler);
return b;
}
var gColor = group();
var colorLabel = document.createElement('label'); colorLabel.textContent = 'Цвет';
var colorInput = document.createElement('input');
colorInput.type = 'color'; colorInput.className = 'dp-color'; colorInput.value = color;
colorInput.addEventListener('input', function () { color = colorInput.value; updateSwatches(); });
gColor.appendChild(colorLabel);
gColor.appendChild(colorInput);
var PALETTE = [
'#000000','#475569','#94a3b8','#ffffff',
'#2b6cff','#0ea5e9','#06b6d4','#10b981',
'#22c55e','#84cc16','#eab308','#f97316',
'#ef4444','#d62828','#ec4899','#a855f7'
];
var swatches = document.createElement('div'); swatches.className = 'dp-swatches';
PALETTE.forEach(function (hex) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'dp-swatch';
b.style.background = hex;
b.dataset.color = hex;
b.title = hex;
b.addEventListener('click', function () {
color = hex; colorInput.value = hex; updateSwatches();
});
swatches.appendChild(b);
});
function updateSwatches() {
Array.prototype.forEach.call(swatches.children, function (el) {
el.classList.toggle('active', el.dataset.color.toLowerCase() === color.toLowerCase());
});
}
updateSwatches();
gColor.appendChild(swatches);
toolbar.appendChild(gColor);
var gWidth = group();
var wLabel = document.createElement('label'); wLabel.textContent = 'Толщина';
var wInput = document.createElement('input');
wInput.type = 'range'; wInput.className = 'dp-range';
wInput.min = '1'; wInput.max = '60'; wInput.value = brush;
wInput.addEventListener('input', function () { brush = parseInt(wInput.value, 10); });
gWidth.appendChild(wLabel); gWidth.appendChild(wInput);
toolbar.appendChild(gWidth);
var gText = group();
var tLabel = document.createElement('label'); tLabel.textContent = 'Текст';
var tSize = document.createElement('input');
tSize.type = 'number'; tSize.className = 'dp-number';
tSize.min = '8'; tSize.max = '200'; tSize.value = fontSize;
tSize.addEventListener('input', function () {
fontSize = Math.max(8, Math.min(200, parseInt(tSize.value, 10) || 24));
});
gText.appendChild(tLabel); gText.appendChild(tSize);
toolbar.appendChild(gText);
var gBg = group();
var bgLabel = document.createElement('label'); bgLabel.textContent = 'Фон';
var bgSelect = document.createElement('select'); bgSelect.className = 'dp-select';
[
['transparent', 'Прозрачный'],
['#ffffff', 'Белый'],
['#000000', 'Чёрный'],
['#f8fafc', 'Светло-серый'],
['#0f172a', 'Тёмно-синий'],
['#7dd3fc', 'Голубой'],
['#fde68a', 'Жёлтый'],
['#bbf7d0', 'Зелёный'],
['#fecaca', 'Красный'],
['#e9d5ff', 'Сиреневый']
].forEach(function (p) {
var o = document.createElement('option'); o.value = p[0]; o.textContent = p[1];
bgSelect.appendChild(o);
});
var bgCustom = document.createElement('input');
bgCustom.type = 'color'; bgCustom.className = 'dp-color'; bgCustom.value = '#ffffff';
bgCustom.title = 'Свой цвет фона';
bgCustom.addEventListener('input', function () { bgFill = bgCustom.value; redraw(); });
bgSelect.addEventListener('change', function () { bgFill = bgSelect.value; redraw(); });
gBg.appendChild(bgLabel); gBg.appendChild(bgSelect); gBg.appendChild(bgCustom);
toolbar.appendChild(gBg);
var gHistory = group();
var undoBtn = mkBtn('Отменить', 'secondary', function () {
if (!items.length) return;
redo.push(items.pop());
redraw();
});
var redoBtn = mkBtn('Вернуть', 'secondary', function () {
if (!redo.length) return;
items.push(redo.pop());
redraw();
});
var clearBtn = mkBtn('Очистить', 'danger', function () {
if (!items.length || confirm('Очистить всё?')) {
items = []; redo = []; redraw();
}
});
gHistory.appendChild(undoBtn); gHistory.appendChild(redoBtn); gHistory.appendChild(clearBtn);
toolbar.appendChild(gHistory);
var gExport = group();
var pngBtn = mkBtn('Скачать PNG', '', function () { downloadPng(); });
var codeBtn = mkBtn('Скопировать скрипт', 'secondary', function () { openScriptModal(); });
var openBtn = mkBtn('Открыть в новом окне', 'secondary', function () { openPreview(); });
gExport.appendChild(pngBtn); gExport.appendChild(codeBtn); gExport.appendChild(openBtn);
toolbar.appendChild(gExport);
var toolDefs = [
['brush', 'Кисть'],
['erase', 'Ластик'],
['shape', 'Фигуры'],
['text', 'Текст']
];
var toolButtons = {};
toolDefs.forEach(function (t) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'dp-tool' + (tool === t[0] ? ' active' : '');
b.textContent = t[1];
b.addEventListener('click', function () {
tool = t[0];
Object.keys(toolButtons).forEach(function (k) {
toolButtons[k].classList.toggle('active', k === tool);
});
shapeRow.style.display = (tool === 'shape') ? 'flex' : 'none';
textRow.style.display = (tool === 'text') ? 'flex' : 'none';
});
toolButtons[t[0]] = b;
toolRow.appendChild(b);
});
var shapeDefs = [
['line', 'Линия'],
['rect', 'Прямоугольник'],
['circle', 'Круг'],
['triangle', 'Треугольник'],
['star', 'Звезда'],
['arrow', 'Стрелка'],
['heart', 'Сердце']
];
var shapeButtons = {};
shapeDefs.forEach(function (s) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'dp-tool' + (shape === s[0] ? ' active' : '');
b.textContent = s[1];
b.addEventListener('click', function () {
shape = s[0];
Object.keys(shapeButtons).forEach(function (k) {
shapeButtons[k].classList.toggle('active', k === shape);
});
});
shapeButtons[s[0]] = b;
shapeRow.appendChild(b);
});
var fillBtn = document.createElement('button');
fillBtn.type = 'button';
fillBtn.className = 'dp-tool';
fillBtn.textContent = 'Заливка';
fillBtn.addEventListener('click', function () {
shapeFill = !shapeFill;
fillBtn.classList.toggle('active', shapeFill);
});
shapeRow.appendChild(fillBtn);
textRow.textContent = 'Кликни по холсту — появится поле ввода. Enter — сохранить, Shift+Enter — новая строка, Esc — отменить.';
var modal = document.createElement('div'); modal.className = 'dp-modal';
var box = document.createElement('div'); box.className = 'dp-modal-box';
var h3 = document.createElement('h3'); h3.textContent = 'Готовый скрипт';
var mh = document.createElement('div'); mh.className = 'dp-hint';
mh.textContent = 'Скопируй и вставь на свой сайт в блок «Произвольный HTML».';
var ta = document.createElement('textarea'); ta.readOnly = true;
var actions = document.createElement('div'); actions.className = 'dp-modal-actions';
var copyBtn = mkBtn('Скопировать', '', function () {
copyText(ta.value);
copyBtn.textContent = 'Скопировано!';
setTimeout(function () { copyBtn.textContent = 'Скопировать'; }, 1400);
});
var closeBtn = mkBtn('Закрыть', 'secondary', function () { modal.classList.remove('open'); });
actions.appendChild(copyBtn); actions.appendChild(closeBtn);
box.appendChild(h3); box.appendChild(mh); box.appendChild(ta); box.appendChild(actions);
modal.appendChild(box);
root.appendChild(modal);
modal.addEventListener('click', function (e) { if (e.target === modal) modal.classList.remove('open'); });
function downloadPng() {
var out = document.createElement('canvas');
out.width = canvas.width;
out.height = canvas.height;
var octx = out.getContext('2d');
if (bgFill !== 'transparent') {
octx.fillStyle = bgFill;
octx.fillRect(0, 0, out.width, out.height);
}
octx.drawImage(canvas, 0, 0);
var url = out.toDataURL('image/png');
var a = document.createElement('a');
a.href = url;
a.download = 'drawing-' + Date.now() + '.png';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setStatus('PNG сохранён.');
}
function openPreview() {
var html = buildStandaloneHtml(packData());
var w = window.open('', '_blank');
if (!w) { setStatus('Разреши всплывающие окна.'); return; }
w.document.open(); w.document.write(html); w.document.close();
}
function packData() {
return {
bg: bgFill,
items: items.map(function (it) {
if (it.type === 'stroke') {
return { type: 'stroke', c: it.c, w: it.w, m: it.m,
p: it.p.map(function (pt) { return [ +pt[0].toFixed(4), +pt[1].toFixed(4) ]; }) };
}
if (it.type === 'shape') {
return { type: 'shape', shape: it.shape, c: it.c, w: it.w, fill: !!it.fill,
p: [ [ +it.p[0][0].toFixed(4), +it.p[0][1].toFixed(4) ],
[ +it.p[1][0].toFixed(4), +it.p[1][1].toFixed(4) ] ] };
}
if (it.type === 'text') {
return { type: 'text', c: it.c, size: it.size, text: it.text,
p: [ +it.p[0].toFixed(4), +it.p[1].toFixed(4) ] };
}
})
};
}
function openScriptModal() {
if (!items.length) { setStatus('Сначала что-нибудь нарисуй.'); return; }
ta.value = buildScript(packData());
modal.classList.add('open');
}
function buildScript(data) {
var json = JSON.stringify(data);
var lines = [];
lines.push('<!-- Drawn Picture -->');
lines.push('<div id="drawn-picture-host" style="position:relative;width:100%;max-width:1100px;height:' + HEIGHT + 'px;margin:0 auto;"></div>');
lines.push('<script>');
lines.push('(function(){');
lines.push(" 'use strict';");
lines.push(' var DATA = ' + json + ';');
lines.push(' var HOST = document.getElementById("drawn-picture-host");');
lines.push(' var canvas = document.createElement("canvas");');
lines.push(' canvas.style.cssText = "display:block;width:100%;height:100%;";');
lines.push(' HOST.appendChild(canvas);');
lines.push(' var ctx = canvas.getContext("2d");');
lines.push(' var W=0,H=0,dpr=Math.min(2,window.devicePixelRatio||1);');
lines.push(' function resize(){W=HOST.clientWidth;H=HOST.clientHeight;canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);ctx.lineCap="round";ctx.lineJoin="round";render();}');
lines.push(' function drawStar(c,cx,cy,spikes,outer,inner,fill){var rot=-Math.PI/2;var step=Math.PI/spikes;c.beginPath();c.moveTo(cx+Math.cos(rot)*outer,cy+Math.sin(rot)*outer);for(var i=0;i<spikes;i++){rot+=step;c.lineTo(cx+Math.cos(rot)*inner,cy+Math.sin(rot)*inner);rot+=step;c.lineTo(cx+Math.cos(rot)*outer,cy+Math.sin(rot)*outer);}c.closePath();if(fill)c.fill();else c.stroke();}');
lines.push(' function drawItem(it){');
lines.push(' if(it.type==="stroke"){');
lines.push(' var pts=it.p;if(!pts.length)return;');
lines.push(' ctx.save();ctx.lineWidth=it.w;');
lines.push(' if(it.m===1){ctx.globalCompositeOperation="destination-out";ctx.strokeStyle="rgba(0,0,0,1)";}');
lines.push(' else{ctx.globalCompositeOperation="source-over";ctx.strokeStyle=it.c;}');
lines.push(' if(pts.length===1){ctx.beginPath();ctx.arc(pts[0][0]*W,pts[0][1]*H,it.w/2,0,Math.PI*2);ctx.fillStyle=ctx.strokeStyle;ctx.fill();}');
lines.push(' else{ctx.beginPath();ctx.moveTo(pts[0][0]*W,pts[0][1]*H);for(var i=1;i<pts.length;i++)ctx.lineTo(pts[i][0]*W,pts[i][1]*H);ctx.stroke();}');
lines.push(' ctx.restore();');
lines.push(' } else if(it.type==="shape"){');
lines.push(' var a=[it.p[0][0]*W,it.p[0][1]*H],b=[it.p[1][0]*W,it.p[1][1]*H];');
lines.push(' var x0=Math.min(a[0],b[0]),y0=Math.min(a[1],b[1]),x1=Math.max(a[0],b[0]),y1=Math.max(a[1],b[1]);');
lines.push(' var w=x1-x0,h=y1-y0;');
lines.push(' ctx.save();ctx.lineWidth=it.w;ctx.strokeStyle=it.c;ctx.fillStyle=it.c;ctx.lineJoin="round";ctx.lineCap="round";');
lines.push(' if(it.shape==="line"){ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();}');
lines.push(' else if(it.shape==="rect"){if(it.fill)ctx.fillRect(x0,y0,w,h);else ctx.strokeRect(x0,y0,w,h);}');
lines.push(' else if(it.shape==="circle"){var cx=(x0+x1)/2,cy=(y0+y1)/2;ctx.beginPath();ctx.ellipse(cx,cy,w/2,h/2,0,0,Math.PI*2);if(it.fill)ctx.fill();else ctx.stroke();}');
lines.push(' else if(it.shape==="triangle"){ctx.beginPath();ctx.moveTo((x0+x1)/2,y0);ctx.lineTo(x1,y1);ctx.lineTo(x0,y1);ctx.closePath();if(it.fill)ctx.fill();else ctx.stroke();}');
lines.push(' else if(it.shape==="star"){drawStar(ctx,(x0+x1)/2,(y0+y1)/2,5,Math.min(w,h)/2,Math.min(w,h)/4,it.fill);}');
lines.push(' else if(it.shape==="arrow"){var ax=a[0],ay=a[1],bx=b[0],by=b[1];var ang=Math.atan2(by-ay,bx-ax);var hl=Math.max(10,it.w*4);ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();ctx.beginPath();ctx.moveTo(bx,by);ctx.lineTo(bx-hl*Math.cos(ang-Math.PI/6),by-hl*Math.sin(ang-Math.PI/6));ctx.lineTo(bx-hl*Math.cos(ang+Math.PI/6),by-hl*Math.sin(ang+Math.PI/6));ctx.closePath();ctx.fill();}');
lines.push(' else if(it.shape==="heart"){var hx=(x0+x1)/2,hy=(y0+y1)/2,hw=w/2,hh=h/2;ctx.beginPath();ctx.moveTo(hx,hy+hh);ctx.bezierCurveTo(hx-hw*2,hy-hh*0.2,hx-hw*0.6,hy-hh*1.6,hx,hy-hh*0.6);ctx.bezierCurveTo(hx+hw*0.6,hy-hh*1.6,hx+hw*2,hy-hh*0.2,hx,hy+hh);ctx.closePath();if(it.fill)ctx.fill();else ctx.stroke();}');
lines.push(' ctx.restore();');
lines.push(' } else if(it.type==="text"){');
lines.push(' ctx.save();ctx.fillStyle=it.c;ctx.font="bold "+it.size+"px -apple-system, \\"Segoe UI\\", Roboto, Arial, sans-serif";ctx.textBaseline="top";var lines=String(it.text).split("\\n");for(var i=0;i<lines.length;i++){ctx.fillText(lines[i],it.p[0]*W,it.p[1]*H+i*it.size*1.2);}ctx.restore();');
lines.push(' }');
lines.push(' }');
lines.push(' function render(){');
lines.push(' ctx.clearRect(0,0,W,H);');
lines.push(' if(DATA.bg && DATA.bg!=="transparent"){ctx.fillStyle=DATA.bg;ctx.fillRect(0,0,W,H);}');
lines.push(' for(var i=0;i<DATA.items.length;i++)drawItem(DATA.items[i]);');
lines.push(' }');
lines.push(' window.addEventListener("resize",resize);resize();');
lines.push('})();');
lines.push('<\/script>');
return lines.join('\n');
}
function buildStandaloneHtml(data) {
var json = JSON.stringify(data);
var height = HEIGHT;
var head = ''
+ '<!doctype html><html><head><meta charset="utf-8">'
+ '<title>Рисунок</title>'
+ '<style>html,body{margin:0;padding:0;background:#eef1f6;font-family:-apple-system,"Segoe UI",Roboto,Arial,sans-serif;}'
+ '.wrap{max-width:1100px;margin:24px auto;padding:16px;background:#fff;border-radius:14px;box-shadow:0 6px 24px rgba(0,0,0,.08);}'
+ '.hint{font-size:12px;color:#6b7280;margin-bottom:10px;}'
+ '#host{position:relative;width:100%;height:' + height + 'px;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;background:#fff;}'
+ '</style></head><body>'
+ '<div class="wrap"><div class="hint">Предпросмотр рисунка. Сохрани как PNG правой кнопкой мыши.</div>'
+ '<div id="host"></div></div>';
var script = ''
+ '<script>(function(){var DATA=' + json + ';'
+ 'var HOST=document.getElementById("host");'
+ 'var canvas=document.createElement("canvas");canvas.style.cssText="display:block;width:100%;height:100%;";HOST.appendChild(canvas);'
+ 'var ctx=canvas.getContext("2d");var W=0,H=0,dpr=Math.min(2,window.devicePixelRatio||1);'
+ 'function drawStar(c,cx,cy,spikes,outer,inner,fill){var rot=-Math.PI/2;var step=Math.PI/spikes;c.beginPath();c.moveTo(cx+Math.cos(rot)*outer,cy+Math.sin(rot)*outer);for(var i=0;i<spikes;i++){rot+=step;c.lineTo(cx+Math.cos(rot)*inner,cy+Math.sin(rot)*inner);rot+=step;c.lineTo(cx+Math.cos(rot)*outer,cy+Math.sin(rot)*outer);}c.closePath();if(fill)c.fill();else c.stroke();}'
+ 'function drawItem(it){'
+ 'if(it.type==="stroke"){var pts=it.p;if(!pts.length)return;ctx.save();ctx.lineWidth=it.w;if(it.m===1){ctx.globalCompositeOperation="destination-out";ctx.strokeStyle="rgba(0,0,0,1)";}else{ctx.globalCompositeOperation="source-over";ctx.strokeStyle=it.c;}if(pts.length===1){ctx.beginPath();ctx.arc(pts[0][0]*W,pts[0][1]*H,it.w/2,0,Math.PI*2);ctx.fillStyle=ctx.strokeStyle;ctx.fill();}else{ctx.beginPath();ctx.moveTo(pts[0][0]*W,pts[0][1]*H);for(var i=1;i<pts.length;i++)ctx.lineTo(pts[i][0]*W,pts[i][1]*H);ctx.stroke();}ctx.restore();}'
+ 'else if(it.type==="shape"){var a=[it.p[0][0]*W,it.p[0][1]*H],b=[it.p[1][0]*W,it.p[1][1]*H];var x0=Math.min(a[0],b[0]),y0=Math.min(a[1],b[1]),x1=Math.max(a[0],b[0]),y1=Math.max(a[1],b[1]);var w=x1-x0,h=y1-y0;ctx.save();ctx.lineWidth=it.w;ctx.strokeStyle=it.c;ctx.fillStyle=it.c;ctx.lineJoin="round";ctx.lineCap="round";'
+ 'if(it.shape==="line"){ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();}'
+ 'else if(it.shape==="rect"){if(it.fill)ctx.fillRect(x0,y0,w,h);else ctx.strokeRect(x0,y0,w,h);}'
+ 'else if(it.shape==="circle"){var cx=(x0+x1)/2,cy=(y0+y1)/2;ctx.beginPath();ctx.ellipse(cx,cy,w/2,h/2,0,0,Math.PI*2);if(it.fill)ctx.fill();else ctx.stroke();}'
+ 'else if(it.shape==="triangle"){ctx.beginPath();ctx.moveTo((x0+x1)/2,y0);ctx.lineTo(x1,y1);ctx.lineTo(x0,y1);ctx.closePath();if(it.fill)ctx.fill();else ctx.stroke();}'
+ 'else if(it.shape==="star"){drawStar(ctx,(x0+x1)/2,(y0+y1)/2,5,Math.min(w,h)/2,Math.min(w,h)/4,it.fill);}'
+ 'else if(it.shape==="arrow"){var ax=a[0],ay=a[1],bx=b[0],by=b[1];var ang=Math.atan2(by-ay,bx-ax);var hl=Math.max(10,it.w*4);ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();ctx.beginPath();ctx.moveTo(bx,by);ctx.lineTo(bx-hl*Math.cos(ang-Math.PI/6),by-hl*Math.sin(ang-Math.PI/6));ctx.lineTo(bx-hl*Math.cos(ang+Math.PI/6),by-hl*Math.sin(ang+Math.PI/6));ctx.closePath();ctx.fill();}'
+ 'else if(it.shape==="heart"){var hx=(x0+x1)/2,hy=(y0+y1)/2,hw=w/2,hh=h/2;ctx.beginPath();ctx.moveTo(hx,hy+hh);ctx.bezierCurveTo(hx-hw*2,hy-hh*0.2,hx-hw*0.6,hy-hh*1.6,hx,hy-hh*0.6);ctx.bezierCurveTo(hx+hw*0.6,hy-hh*1.6,hx+hw*2,hy-hh*0.2,hx,hy+hh);ctx.closePath();if(it.fill)ctx.fill();else ctx.stroke();}'
+ 'ctx.restore();}'
+ 'else if(it.type==="text"){ctx.save();ctx.fillStyle=it.c;ctx.font="bold "+it.size+"px -apple-system, \\"Segoe UI\\", Roboto, Arial, sans-serif";ctx.textBaseline="top";var lines=String(it.text).split("\\n");for(var i=0;i<lines.length;i++){ctx.fillText(lines[i],it.p[0]*W,it.p[1]*H+i*it.size*1.2);}ctx.restore();}'
+ '}'
+ 'function render(){ctx.clearRect(0,0,W,H);if(DATA.bg && DATA.bg!=="transparent"){ctx.fillStyle=DATA.bg;ctx.fillRect(0,0,W,H);}for(var i=0;i<DATA.items.length;i++)drawItem(DATA.items[i]);}'
+ 'function resize(){W=HOST.clientWidth;H=HOST.clientHeight;canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);ctx.lineCap="round";ctx.lineJoin="round";render();}'
+ 'window.addEventListener("resize",resize);resize();})();<\/script>';
return head + script + '</body></html>';
}
function setStatus(txt) {
status.textContent = txt || '';
if (!txt) return;
clearTimeout(setStatus._t);
setStatus._t = setTimeout(function () { status.textContent = ''; }, 2500);
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () { fallbackCopy(text); });
return;
}
fallbackCopy(text);
}
function fallbackCopy(text) {
var t = document.createElement('textarea');
t.value = text;
t.style.position = 'fixed';
t.style.left = '-9999px';
document.body.appendChild(t);
t.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(t);
}
resize();
console.log('[DrawnPicture] Готово: рисовалка инициализирована');
}
function initAll() {
var list = document.querySelectorAll('.drawn-picture-app');
console.log('[DrawnPicture] Найдено контейнеров:', list.length);
for (var i = 0; i < list.length; i++) initApp(list[i]);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();
</script>
<?php
}
}
new DrawnPicturePlugin();
Скачать плагин: ссылка
Добавить комментарий