kingkont 0.7.84 → 0.7.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ beautiful Cyrillic-aware label nodes without needing a network connection.
14
14
  | Comfortaa | Johan Aakerlund | <https://fonts.google.com/specimen/Comfortaa> |
15
15
  | PT Serif | ParaType | <https://fonts.google.com/specimen/PT+Serif> |
16
16
  | PT Mono | ParaType | <https://fonts.google.com/specimen/PT+Mono> |
17
+ | Neucha | Jovanny Lemonad | <https://fonts.google.com/specimen/Neucha> |
17
18
 
18
19
  Files were downloaded from the @fontsource jsDelivr mirror in the
19
20
  `cyrillic-400-normal` and `latin-400-normal` subsets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.84",
3
+ "version": "0.7.86",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1773,6 +1773,38 @@ function showNodeContextMenu(node, clientX, clientY) {
1773
1773
  menu.appendChild(b);
1774
1774
  };
1775
1775
  add(node.name ? `✏ Переименовать (${node.name})` : '✏ Переименовать', () => renameNode(node));
1776
+ // Label: пункты для смены стиля (шрифт / размер / курсив).
1777
+ // Сделано через ПКМ а не постоянный hover-тулбар, чтобы UI label-ноды
1778
+ // оставался «чистым текстом» без фурнитуры.
1779
+ if (node.type === 'label') {
1780
+ if (!node.textStyle) node.textStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
1781
+ const ts = node.textStyle;
1782
+ const curFont = LABEL_FONTS.find(f => f.id === ts.fontFamily) || LABEL_FONTS[0];
1783
+ add(`🔤 Шрифт: ${curFont.label}`, async () => {
1784
+ const choice = await askChoice('Шрифт label-ноды', LABEL_FONTS.map(f => f.label), curFont.label);
1785
+ if (!choice) return;
1786
+ const picked = LABEL_FONTS.find(f => f.label === choice);
1787
+ if (!picked) return;
1788
+ ts.fontFamily = picked.id;
1789
+ scheduleSave();
1790
+ await refreshNodeDOM(node.id);
1791
+ });
1792
+ add(`📏 Размер: ${ts.fontSize || 32}px`, async () => {
1793
+ const choice = await askChoice('Размер шрифта', LABEL_FONT_SIZES.map(s => s + 'px'), (ts.fontSize || 32) + 'px');
1794
+ if (!choice) return;
1795
+ const n = parseInt(choice, 10);
1796
+ if (Number.isFinite(n)) {
1797
+ ts.fontSize = n;
1798
+ scheduleSave();
1799
+ await refreshNodeDOM(node.id);
1800
+ }
1801
+ });
1802
+ add(ts.italic ? '𝐼 Курсив: вкл' : '𝐼 Курсив: выкл', async () => {
1803
+ ts.italic = !ts.italic;
1804
+ scheduleSave();
1805
+ await refreshNodeDOM(node.id);
1806
+ });
1807
+ }
1776
1808
  add('📋 Логи', () => showNodeLogs(node));
1777
1809
  if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
1778
1810
  if (node.status === 'generating') {
@@ -211,7 +211,7 @@ async function addLabelAt(pos) {
211
211
  id: crypto.randomUUID(),
212
212
  type: 'label',
213
213
  text: '',
214
- textStyle: { fontSize: 32, italic: false, fontFamily: 'handwritten' },
214
+ textStyle: { fontSize: 32, italic: false, fontFamily: 'pencil' },
215
215
  x, y,
216
216
  // width/height не задаём — label всегда auto-размер по тексту
217
217
  // (см. createNodeEl: для type='label' inline-style не применяется).
@@ -66,7 +66,8 @@ $('settingsRegen').addEventListener('click', () => {
66
66
  // сохранённые сцены. Новые красивые шрифты добавляем под новыми id.
67
67
  const LABEL_FONTS = [
68
68
  { id: 'default', label: 'Обычный' },
69
- { id: 'handwritten', label: 'Карандашом' }, // Caveat
69
+ { id: 'pencil', label: 'Грифель' }, // Neucha — шероховатый карандашный
70
+ { id: 'handwritten', label: 'Прописью' }, // Caveat — гладкий рукописный
70
71
  { id: 'brush', label: 'Скорописью' }, // Bad Script
71
72
  { id: 'marker', label: 'Кистью' }, // Pacifico
72
73
  { id: 'display', label: 'Декор' }, // Lobster
@@ -87,73 +88,56 @@ function applyLabelStyle(ed, style) {
87
88
  function renderLabelNodeBody(node, body) {
88
89
  if (!node.textStyle) node.textStyle = { ...LABEL_DEFAULT_STYLE };
89
90
 
90
- // Тулбар форматирования (на hover, чтобы не отвлекать от чистого текста).
91
- const tb = document.createElement('div');
92
- tb.className = 'label-toolbar';
93
- tb.addEventListener('mousedown', e => e.stopPropagation());
94
-
95
- const fontSel = document.createElement('select');
96
- fontSel.title = 'Шрифт';
97
- for (const f of LABEL_FONTS) {
98
- const o = document.createElement('option');
99
- o.value = f.id; o.textContent = f.label;
100
- fontSel.appendChild(o);
101
- }
102
- fontSel.value = node.textStyle.fontFamily || 'default';
103
- fontSel.addEventListener('change', () => {
104
- node.textStyle.fontFamily = fontSel.value;
105
- applyLabelStyle(ed, node.textStyle);
106
- scheduleSave();
107
- });
108
- tb.appendChild(fontSel);
109
-
110
- const sizeSel = document.createElement('select');
111
- sizeSel.title = 'Размер шрифта';
112
- for (const s of LABEL_FONT_SIZES) {
113
- const o = document.createElement('option');
114
- o.value = String(s); o.textContent = s + 'px';
115
- sizeSel.appendChild(o);
116
- }
117
- sizeSel.value = String(node.textStyle.fontSize || 32);
118
- sizeSel.addEventListener('change', () => {
119
- node.textStyle.fontSize = parseInt(sizeSel.value, 10) || 32;
120
- applyLabelStyle(ed, node.textStyle);
121
- scheduleSave();
122
- });
123
- tb.appendChild(sizeSel);
124
-
125
- const italicBtn = document.createElement('button');
126
- italicBtn.className = 'tt-italic';
127
- italicBtn.textContent = 'I';
128
- italicBtn.title = 'Курсив';
129
- if (node.textStyle.italic) italicBtn.classList.add('active');
130
- italicBtn.addEventListener('click', e => {
131
- e.stopPropagation();
132
- node.textStyle.italic = !node.textStyle.italic;
133
- italicBtn.classList.toggle('active', node.textStyle.italic);
134
- applyLabelStyle(ed, node.textStyle);
135
- scheduleSave();
136
- });
137
- tb.appendChild(italicBtn);
138
-
139
- body.appendChild(tb);
140
-
141
- // Сам текст — contenteditable (без рамок, без скроллбаров, ровно как
142
- // плавающая надпись на холсте). plaintext-only — режем форматирование
143
- // из буфера обмена (текст копируется только как текст).
91
+ // Сам текст. По умолчанию НЕ contenteditable обычный <div>. Это значит:
92
+ // • single-click = выделение ноды (drag handler из attachDrag)
93
+ // • ПКМ = node-context-menu (ниже добавлены пункты «Шрифт/Размер/Курсив»)
94
+ // • dblclick = вход в режим редактирования
95
+ // • blur = выход из редактирования
96
+ // Шрифт/размер/курсив меняются через ПКМ (а не через постоянный hover-тулбар),
97
+ // чтобы UI label-ноды был «чистый текст без фурнитуры».
144
98
  const ed = document.createElement('div');
145
99
  ed.className = 'label-text';
146
- ed.contentEditable = 'plaintext-only';
147
100
  ed.spellcheck = false;
148
- ed.dataset.placeholder = 'Текст…';
101
+ ed.dataset.placeholder = 'Двойной клик — редактировать';
149
102
  ed.textContent = node.text || '';
150
103
  applyLabelStyle(ed, node.textStyle);
104
+
105
+ const enterEditMode = () => {
106
+ if (ed.isContentEditable) return;
107
+ ed.contentEditable = 'plaintext-only';
108
+ ed.focus();
109
+ // Курсор в конец (если контент пустой — это и есть начало).
110
+ const sel = window.getSelection();
111
+ const r = document.createRange();
112
+ r.selectNodeContents(ed);
113
+ r.collapse(false);
114
+ sel?.removeAllRanges();
115
+ sel?.addRange(r);
116
+ };
117
+ const exitEditMode = () => {
118
+ if (!ed.isContentEditable) return;
119
+ // removeAttribute полностью убирает contenteditable, чтобы
120
+ // closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
121
+ // не матчился — и ПКМ снова открывал наш node-menu.
122
+ ed.removeAttribute('contenteditable');
123
+ };
124
+
125
+ ed.addEventListener('dblclick', e => {
126
+ e.stopPropagation();
127
+ enterEditMode();
128
+ });
129
+ ed.addEventListener('blur', exitEditMode);
151
130
  ed.addEventListener('input', () => { node.text = ed.textContent; scheduleSave(); });
152
- ed.addEventListener('mousedown', e => e.stopPropagation());
153
- // Enter — перенос строки. Esc — снять фокус.
154
131
  ed.addEventListener('keydown', e => {
155
132
  if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
156
133
  });
134
+ ed.addEventListener('mousedown', e => {
135
+ // В edit-режиме: не пускаем drag-обработчик (нужно поставить курсор/выделить текст).
136
+ if (ed.isContentEditable) e.stopPropagation();
137
+ // В read-режиме: пузырится — attachDrag-handler (повешен и на label-text)
138
+ // обработает single-click как «выбрать ноду» и подхватит drag.
139
+ });
140
+
157
141
  body.appendChild(ed);
158
142
  }
159
143
 
@@ -618,6 +602,42 @@ function attachResize(el, node, handle) {
618
602
  const startX = e.clientX, startY = e.clientY;
619
603
  const startW = el.offsetWidth, startH = el.offsetHeight;
620
604
 
605
+ // Label: ресайз-хендл МАСШТАБИРУЕТ шрифт, а не меняет размеры бокса.
606
+ // Бокс auto-sized по тексту, поэтому при увеличении fontSize он
607
+ // автоматически растёт. Используем диагональную дистанцию (max от dx/dy)
608
+ // — даёт интуитивный «оттянуть угол → больше».
609
+ if (node.type === 'label') {
610
+ if (!node.textStyle) node.textStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
611
+ const startSize = node.textStyle.fontSize || 32;
612
+ const labelTextEl = el.querySelector('.label-text');
613
+ const sizeSel = el.querySelector('.label-toolbar select[title="Размер шрифта"]');
614
+ const onMoveL = ev => {
615
+ const dx = (ev.clientX - startX) / state.zoom;
616
+ const dy = (ev.clientY - startY) / state.zoom;
617
+ // Линейная зависимость: ~0.6px шрифта на пиксель диагонального drag.
618
+ // Берём «среднее с уклоном на больший» — чтобы юзер мог тянуть как
619
+ // вправо-вниз (увеличивать), так и влево-вверх (уменьшать).
620
+ const delta = (dx + dy) * 0.6;
621
+ const newSize = Math.max(8, Math.min(200, Math.round(startSize + delta)));
622
+ node.textStyle.fontSize = newSize;
623
+ if (labelTextEl) labelTextEl.style.fontSize = newSize + 'px';
624
+ // Синхронизируем dropdown в тулбаре, если есть подходящий option.
625
+ if (sizeSel) {
626
+ const has = Array.from(sizeSel.options).some(o => o.value === String(newSize));
627
+ if (has) sizeSel.value = String(newSize);
628
+ }
629
+ renderConnections();
630
+ };
631
+ const onUpL = () => {
632
+ document.removeEventListener('mousemove', onMoveL);
633
+ document.removeEventListener('mouseup', onUpL);
634
+ scheduleSave();
635
+ };
636
+ document.addEventListener('mousemove', onMoveL);
637
+ document.addEventListener('mouseup', onUpL);
638
+ return;
639
+ }
640
+
621
641
  // Aspect-lock для image/video: соблюдаем ratio оригинала. Берём nat-
622
642
  // ural размеры media-элемента, считаем chrome (header + footer + padding)
623
643
  // как разницу высоты ноды и высоты media. На этапе resize меняем ту ось,
@@ -674,7 +694,17 @@ function attachResize(el, node, handle) {
674
694
 
675
695
  function attachDrag(el, node) {
676
696
  const header = el.querySelector('.node-header');
677
- header.addEventListener('mousedown', e => {
697
+ // Для label-нод вешаем drag и на сам текст: header скрыт до hover, а
698
+ // юзер хочет цеплять ноду с любого места видимого текста. В handler'е
699
+ // пропускаем срабатывание, если label в edit-режиме (см. условие ниже).
700
+ const sources = [header];
701
+ if (node.type === 'label') {
702
+ const lt = el.querySelector('.label-text');
703
+ if (lt) sources.push(lt);
704
+ }
705
+ const handler = e => {
706
+ // Edit-режим label — не таскаем (контекст редактирования текста).
707
+ if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
678
708
  if (e.target.closest('.delete')) return;
679
709
  // Multi-select c модификаторами — без drag
680
710
  if (e.metaKey || e.ctrlKey || e.shiftKey) {
@@ -811,7 +841,8 @@ function attachDrag(el, node) {
811
841
  };
812
842
  document.addEventListener('mousemove', onMove);
813
843
  document.addEventListener('mouseup', onUp);
814
- });
844
+ };
845
+ for (const src of sources) if (src) src.addEventListener('mousedown', handler);
815
846
  }
816
847
 
817
848
  // Добавить ноду как клип в указанную дорожку с ripple-вставкой по времени
@@ -82,6 +82,16 @@
82
82
  src: url('../assets/fonts/pt-mono-latin-400.woff2') format('woff2');
83
83
  unicode-range: U+0000-00FF;
84
84
  }
85
+ @font-face {
86
+ font-family: 'KK Neucha'; font-style: normal; font-weight: 400; font-display: block;
87
+ src: url('../assets/fonts/neucha-cyrillic-400.woff2') format('woff2');
88
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
89
+ }
90
+ @font-face {
91
+ font-family: 'KK Neucha'; font-style: normal; font-weight: 400; font-display: block;
92
+ src: url('../assets/fonts/neucha-latin-400.woff2') format('woff2');
93
+ unicode-range: U+0000-00FF;
94
+ }
85
95
 
86
96
  * { box-sizing: border-box; margin: 0; padding: 0; }
87
97
  html, body { height: 100%; }
@@ -450,9 +460,23 @@
450
460
  }
451
461
  /* Footer для label не используется (нет сгенерированной информации) — скрываем. */
452
462
  .node.label-node .node-footer { display: none; }
453
- /* Resize и anchor для label не нужнытекст auto-размером, ссылок нет. */
454
- .node.label-node .resize-handle,
463
+ /* Anchor для label не нуженэто просто аннотация, не источник для генерации. */
455
464
  .node.label-node .anchor { display: none; }
465
+ /* Resize-handle для label НЕ ресайзит ширину/высоту, а МАСШТАБИРУЕТ шрифт.
466
+ Видимый правый-нижний уголок, hover-only, чтобы не отвлекать. */
467
+ .node.label-node .resize-handle {
468
+ position: absolute;
469
+ right: -2px; bottom: -2px;
470
+ width: 14px; height: 14px;
471
+ background: rgba(90,168,255,0.85);
472
+ border: 1px solid rgba(255,255,255,0.4);
473
+ border-radius: 3px;
474
+ cursor: nwse-resize;
475
+ opacity: 0; transition: opacity 0.15s;
476
+ z-index: 11;
477
+ }
478
+ .node.label-node:hover .resize-handle,
479
+ .node.label-node:focus-within .resize-handle { opacity: 1; }
456
480
 
457
481
  /* Body — wrapper без визуального вклада. */
458
482
  .node.label-node .node-body {
@@ -461,62 +485,56 @@
461
485
  position: relative;
462
486
  }
463
487
 
464
- /* Тулбар форматирования absolute под текстом, hover-only. */
465
- .node.label-node .label-toolbar {
466
- position: absolute;
467
- top: 100%; /* под label */
468
- left: 0;
469
- margin-top: 4px;
470
- display: flex; align-items: center; gap: 4px;
471
- padding: 3px 6px; background: rgba(20,20,20,0.92);
472
- border-radius: 4px;
473
- font-size: 11px; flex-shrink: 0;
474
- opacity: 0; transition: opacity 0.15s;
475
- pointer-events: none;
476
- white-space: nowrap;
477
- z-index: 10;
478
- }
488
+ /* Тулбар (.label-toolbar) больше не используется стиль/шрифт меняются
489
+ через ПКМ. Видимый header показываем только на hover. */
479
490
  .node.label-node:hover .node-header,
480
- .node.label-node:hover .label-toolbar,
481
- .node.label-node:focus-within .node-header,
482
- .node.label-node:focus-within .label-toolbar {
491
+ .node.label-node:focus-within .node-header {
483
492
  opacity: 1; pointer-events: auto;
484
493
  }
485
- .node.label-node .label-toolbar select,
486
- .node.label-node .label-toolbar button {
487
- background: #1e1e1e; border: 1px solid #383838; color: #ccc;
488
- border-radius: 3px; padding: 2px 6px; font-size: 11px;
489
- cursor: pointer; line-height: 1.2;
490
- }
491
- .node.label-node .label-toolbar select:hover,
492
- .node.label-node .label-toolbar button:hover { background: #2c2c2c; color: #fff; }
493
- .node.label-node .label-toolbar button.active {
494
- background: #3a5a8a; color: #fff; border-color: #4a6a9a;
495
- }
496
- .node.label-node .label-toolbar .tt-italic { font-style: italic; min-width: 22px; }
497
494
 
498
- /* Сам текст — auto-ширина по контенту, до max-width. */
495
+ /* Сам текст — auto-ширина по контенту, до max-width.
496
+ line-height: 1.5 — чтобы descender'ы рукописных шрифтов (Caveat g/p/y,
497
+ Pacifico, Bad Script) не клипались по нижнему краю. padding 8px вверху
498
+ и внизу — на случай если у конкретной семьи descender лезет ещё дальше. */
499
499
  .node.label-node .label-text {
500
500
  display: inline-block;
501
- padding: 4px 8px;
502
- color: #eaeaea; line-height: 1.2;
501
+ padding: 8px 10px;
502
+ color: #eaeaea; line-height: 1.5;
503
503
  word-break: break-word; white-space: pre-wrap;
504
504
  outline: none; cursor: text;
505
505
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
506
506
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
507
- min-width: 30px;
507
+ min-width: 60px;
508
508
  max-width: 100%;
509
+ /* min-height в em — гарантирует что пустая нода (только placeholder)
510
+ всё равно имеет высоту хотя бы одной строки выбранного шрифта. */
511
+ min-height: 1.5em;
509
512
  }
510
513
  .node.label-node .label-text:empty::before {
511
514
  content: attr(data-placeholder);
512
- color: rgba(200,200,200,0.35);
513
- }
515
+ color: rgba(200,200,200,0.55);
516
+ }
517
+ /* Когда label пустая — рисуем подсказывающий dashed-контур, чтобы юзер
518
+ видел что нода существует (текст-плейсхолдер мог быть не замечен).
519
+ При наличии текста — никакой рамки, чисто подпись на холсте. */
520
+ .node.label-node:has(.label-text:empty) {
521
+ outline: 1px dashed rgba(200,200,200,0.25);
522
+ outline-offset: 1px;
523
+ border-radius: 3px;
524
+ }
525
+ /* Курсор «I-beam» над текстом — на hover показываем «текстуальную
526
+ природу» ноды, в read-mode сигнал что dblclick → редактировать. */
527
+ .node.label-node .label-text { cursor: text; }
528
+ .node.label-node:hover .label-text:not([contenteditable]) { cursor: pointer; }
514
529
  /* === Label-шрифты ===
515
530
  Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
531
+ .node.label-node .label-text[data-font="pencil"] {
532
+ /* Грифель — шероховатый «как написано карандашом» (Neucha). */
533
+ font-family: 'KK Neucha', 'Neucha', 'Marker Felt', 'Comic Sans MS', cursive;
534
+ }
516
535
  .node.label-node .label-text[data-font="handwritten"] {
517
- /* Карандашоммягкий рукописный (Caveat). */
536
+ /* Прописьюгладкий рукописный (Caveat). */
518
537
  font-family: 'KK Caveat', 'Caveat', 'Marker Felt', cursive;
519
- line-height: 1.1;
520
538
  }
521
539
  .node.label-node .label-text[data-font="brush"] {
522
540
  /* Скорописью — каллиграфия в русском стиле (Bad Script). */