mirror of
https://github.com/viliusle/miniPaint.git
synced 2026-02-06 13:51:51 +00:00
Implement layer details
This commit is contained in:
parent
ca69919c26
commit
7b1338c580
@ -43,6 +43,7 @@
|
||||
"hermite-resize": "git+https://github.com/viliusle/Hermite-resize.git",
|
||||
"jquery": "^3.5.1",
|
||||
"pica": "^5.3.0",
|
||||
"terser": "^3.17.0"
|
||||
"terser": "^3.17.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
}
|
||||
}
|
||||
|
||||
@ -479,6 +479,7 @@ body .sp-preview{
|
||||
height: 23px;
|
||||
}
|
||||
.block.details button{
|
||||
width: calc(100% - 70px);
|
||||
height: 23px;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ config.pixabay_key = '3ca2cd8af3fde33af218bea02-9021417';
|
||||
config.layers = [];
|
||||
config.layer = null;
|
||||
config.need_render = false;
|
||||
config.need_render_changed_params = false; // Set specifically when param change in layer details triggered render
|
||||
config.mouse = {};
|
||||
|
||||
//requires styles in reset.css
|
||||
@ -25,6 +26,45 @@ config.themes = [
|
||||
'green',
|
||||
];
|
||||
|
||||
config.FONTS = [
|
||||
"Arial",
|
||||
"Courier",
|
||||
"Impact",
|
||||
"Helvetica",
|
||||
"Monospace",
|
||||
"Tahoma",
|
||||
"Times New Roman",
|
||||
"Verdana",
|
||||
"Amatic SC",
|
||||
"Arimo",
|
||||
"Codystar",
|
||||
"Creepster",
|
||||
"Indie Flower",
|
||||
"Lato",
|
||||
"Lora",
|
||||
"Merriweather",
|
||||
"Monoton",
|
||||
"Montserrat",
|
||||
"Mukta",
|
||||
"Muli",
|
||||
"Nosifer",
|
||||
"Nunito",
|
||||
"Oswald",
|
||||
"Orbitron",
|
||||
"Pacifico",
|
||||
"PT Sans",
|
||||
"PT Serif",
|
||||
"Playfair Display",
|
||||
"Poppins",
|
||||
"Raleway",
|
||||
"Roboto",
|
||||
"Rubik",
|
||||
"Special Elite",
|
||||
"Tangerine",
|
||||
"Titillium Web",
|
||||
"Ubuntu"
|
||||
];
|
||||
|
||||
config.TOOLS = [
|
||||
{
|
||||
name: 'select',
|
||||
@ -136,7 +176,7 @@ config.TOOLS = [
|
||||
attributes: {
|
||||
font: {
|
||||
value: 'Arial',
|
||||
values: ['Arial'],
|
||||
values: ['', ...config.FONTS.sort()],
|
||||
},
|
||||
size: 40,
|
||||
bold: {
|
||||
@ -248,6 +288,5 @@ config.TOOLS = [
|
||||
|
||||
//link to active tool
|
||||
config.TOOL = config.TOOLS[2];
|
||||
|
||||
|
||||
export default config;
|
||||
@ -102,6 +102,7 @@ class Base_layers_class {
|
||||
|
||||
after_render() {
|
||||
config.need_render = false;
|
||||
config.need_render_changed_params = false;
|
||||
this.ctx.restore();
|
||||
zoomView.canvasDefault();
|
||||
}
|
||||
|
||||
@ -40,33 +40,59 @@ var template = `
|
||||
<span class="trn label">Color:</span>
|
||||
<input style="padding: 0px;" type="color" id="detail_color" />
|
||||
</div>
|
||||
<div id="params_details">
|
||||
<div id="text_detail_params">
|
||||
<hr />
|
||||
<div class="row">
|
||||
<span class="trn label"> </span>
|
||||
<button type="button" class="trn dots" id="detail_param_text">Edit text...</button>
|
||||
<button type="button" class="trn dots" id="detail_param_bold">Bold</button>
|
||||
<button type="button" class="trn dots" id="detail_param_italic">Italic</button>
|
||||
<button type="button" class="trn dots" id="detail_param_stroke">Stroke</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="trn label">Size:</span>
|
||||
<input type="number" min="1" id="detail_param_size" />
|
||||
<span class="trn label" title="Resize Boundary">Bounds:</span>
|
||||
<select id="detail_param_boundary">
|
||||
<option value="box">Box</option>
|
||||
<option value="dynamic">Dynamic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="trn label">Align:</span>
|
||||
<select id="detail_param_align">
|
||||
<option value="Left">Left</option>
|
||||
<option value="Center">Center</option>
|
||||
<option value="Right">Right</option>
|
||||
<div class="row" hidden> <!-- Future implementation -->
|
||||
<span class="trn label">Direction:</span>
|
||||
<select id="detail_param_text_direction">
|
||||
<option value="ltr">Left to Right</option>
|
||||
<option value="rtl">Right to Left</option>
|
||||
<option value="ttb">Top to Bottom</option>
|
||||
<option value="btt">Bottom to Top</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" hidden> <!-- Future implementation -->
|
||||
<span class="trn label">Wrap:</span>
|
||||
<select id="detail_param_wrap_direction">
|
||||
<option value="ltr">Left to Right</option>
|
||||
<option value="rtl">Right to Left</option>
|
||||
<option value="ttb">Top to Bottom</option>
|
||||
<option value="btt">Bottom to Top</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="trn label">Font:</span>
|
||||
<select id="detail_param_family"></select>
|
||||
<span class="trn label">Wrap At:</span>
|
||||
<select id="detail_param_wrap">
|
||||
<option value="letter">Word + Letter</option>
|
||||
<option value="word">Word</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="trn label">Stroke:</span>
|
||||
<input type="number" min="0" id="detail_param_stroke_size" />
|
||||
<span class="trn label" title="Horizontal Alignment">H. Align:</span>
|
||||
<select id="detail_param_halign">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" hidden> <!-- Future implementation -->
|
||||
<span class="trn label" title="Vertical Alignment">V. Align:</span>
|
||||
<select id="detail_param_valign">
|
||||
<option value="top">Top</option>
|
||||
<option value="middle">Middle</option>
|
||||
<option value="bottom">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
`;
|
||||
@ -100,19 +126,18 @@ class GUI_details_class {
|
||||
|
||||
//text - special case
|
||||
if (config.layer != undefined && config.layer.type == 'text') {
|
||||
document.getElementById('params_details').style.display = 'block';
|
||||
document.getElementById('text_detail_params').style.display = 'block';
|
||||
}
|
||||
else {
|
||||
document.getElementById('params_details').style.display = 'none';
|
||||
document.getElementById('text_detail_params').style.display = 'none';
|
||||
}
|
||||
this.render_text(events);
|
||||
this.render_general_param('size', events);
|
||||
this.render_general_param('bold', events);
|
||||
this.render_general_param('italic', events);
|
||||
this.render_general_param('stroke', events);
|
||||
this.render_general_param('stroke_size', events);
|
||||
this.render_general_select_param('align', events);
|
||||
this.render_general_select_param('family', events);
|
||||
this.render_general_select_param('boundary', events);
|
||||
this.render_general_select_param('text_direction', events);
|
||||
this.render_general_select_param('wrap', events);
|
||||
this.render_general_select_param('wrap_direction', events);
|
||||
this.render_general_select_param('halign', events);
|
||||
this.render_general_select_param('valign', events);
|
||||
}
|
||||
|
||||
render_general(key, events) {
|
||||
@ -208,6 +233,8 @@ class GUI_details_class {
|
||||
var value = parseInt(this.value);
|
||||
config.layer.params[key] = value;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
|
||||
});
|
||||
document.getElementById('detail_param_' + key).addEventListener('click', function (e) {
|
||||
if (typeof config.layer.params[key] != 'boolean')
|
||||
@ -215,6 +242,7 @@ class GUI_details_class {
|
||||
this.classList.toggle('active');
|
||||
config.layer.params[key] = !config.layer.params[key];
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -244,6 +272,7 @@ class GUI_details_class {
|
||||
var value = this.value;
|
||||
config.layer.params[key] = value;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -264,6 +293,7 @@ class GUI_details_class {
|
||||
var value = this.value;
|
||||
config.layer.color = value;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -290,16 +320,19 @@ class GUI_details_class {
|
||||
if(config.layer.x != null)
|
||||
config.layer.x = 0;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
document.getElementById('reset_y').addEventListener('click', function (e) {
|
||||
if(config.layer.x != null)
|
||||
config.layer.y = 0;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
document.getElementById('reset_size').addEventListener('click', function (e) {
|
||||
config.layer.width = config.layer.width_original;
|
||||
config.layer.height = config.layer.height_original;
|
||||
config.need_render = true;
|
||||
config.need_render_changed_params = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -308,34 +341,12 @@ class GUI_details_class {
|
||||
* item: text
|
||||
*/
|
||||
render_text(events) {
|
||||
var _this = this;
|
||||
var layer = config.layer;
|
||||
|
||||
if (events) {
|
||||
//events
|
||||
document.getElementById('detail_param_text').addEventListener('click', function (e) {
|
||||
var settings = {
|
||||
title: 'Edit text',
|
||||
params: [
|
||||
{name: "text", title: "Text:", value: config.layer.params.text || "", type: "textarea"},
|
||||
],
|
||||
on_finish: function (params) {
|
||||
config.layer.params.text = params.text;
|
||||
config.need_render = true;
|
||||
},
|
||||
};
|
||||
_this.POP.show(settings);
|
||||
document.querySelector('#tools_container #text').click();
|
||||
document.getElementById('text_tool_keyboard_input').focus();
|
||||
});
|
||||
|
||||
//also show font families
|
||||
var families = this.Text.get_fonts();
|
||||
var select = document.getElementById('detail_param_family');
|
||||
for(var i in families){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = families[i];
|
||||
opt.innerHTML = families[i];
|
||||
select.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import Hermite_class from 'hermite-resize';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
import Pica from './../../../../node_modules/pica/dist/pica.js';
|
||||
import Helper_class from './../../libs/helpers.js';
|
||||
import { metaDefaults as textMetaDefaults } from '../../tools/text.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
@ -134,10 +135,15 @@ class Image_resize_class {
|
||||
|
||||
//is text
|
||||
if(layer.type == 'text'){
|
||||
var ratio = width / layer.width;
|
||||
var xratio = width / layer.width;
|
||||
for (let line of layer.data) {
|
||||
for (let span of line) {
|
||||
span.meta.size = Math.ceil((span.meta.size || textMetaDefaults.size) * xratio);
|
||||
span.meta.kerning = Math.ceil((span.meta.kerning || textMetaDefaults.kerning) * xratio);
|
||||
}
|
||||
}
|
||||
layer.width = width;
|
||||
layer.height = height;
|
||||
layer.params.size = Math.ceil(layer.params.size * ratio);
|
||||
this.resize_gui();
|
||||
config.need_render = true;
|
||||
return true;
|
||||
|
||||
@ -6,23 +6,19 @@ import Base_layers_class from './../core/base-layers.js';
|
||||
import GUI_tools_class from './../core/gui/gui-tools.js';
|
||||
import Helper_class from './../libs/helpers.js';
|
||||
import Dialog_class from './../libs/popup.js';
|
||||
import { timers } from 'jquery';
|
||||
import WebFont from 'webfontloader';
|
||||
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* - Font selection
|
||||
* - Rendering underlines
|
||||
* - Rendering strikethrough
|
||||
* - Text horizontal alignment
|
||||
* - Edit text button
|
||||
* - Word/letter wrap setting
|
||||
* - Fix decimal place in number input
|
||||
* - Selection tool change dynamic to box
|
||||
* - Undo history
|
||||
*/
|
||||
|
||||
// Default text styling
|
||||
// WARNING - changing this could break backwards compatibility! Defaults aren't saved in text layer.
|
||||
const metaDefaults = {
|
||||
// WARNING - changing this could break backwards compatibility!
|
||||
// Defaults aren't saved in text layer in order to reduce data size and increase meta comparison performance.
|
||||
export const metaDefaults = {
|
||||
size: 40,
|
||||
family: 'Arial',
|
||||
kerning: 0,
|
||||
@ -38,6 +34,15 @@ const metaDefaults = {
|
||||
// Global map of font name to font metrics information.
|
||||
const fontMetricsMap = new Map();
|
||||
const layerEditors = new WeakMap();
|
||||
const fontLoadMap = new Map();
|
||||
fontLoadMap.set('Arial', true);
|
||||
fontLoadMap.set('Courier', true);
|
||||
fontLoadMap.set('Impact', true);
|
||||
fontLoadMap.set('Helvetica', true);
|
||||
fontLoadMap.set('Monospace', true);
|
||||
fontLoadMap.set('Tahoma', true);
|
||||
fontLoadMap.set('Times New Roman', true);
|
||||
fontLoadMap.set('Verdana', true);
|
||||
|
||||
/**
|
||||
* The canvas's native font metrics implementation doesn't really give us enough information...
|
||||
@ -82,30 +87,6 @@ class Font_metrics_class {
|
||||
*/
|
||||
class Text_document_class {
|
||||
constructor() {
|
||||
/*
|
||||
Text is stored as an array of lines. Each line is an array that contains text span objects (represents a substring of text that has the same format as surrounding text)
|
||||
Example of a document with a single line and single text span (meta that is default value would be omitted, just showing possible options):
|
||||
[
|
||||
[
|
||||
{
|
||||
text: 'Hello World!',
|
||||
meta: {
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
size: 12,
|
||||
family: 'Arial',
|
||||
fill_color: '#000000ff',
|
||||
stroke_color: '#000000ff',
|
||||
stroke_size: 0,
|
||||
kerning: 0,
|
||||
baseline: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
*/
|
||||
this.lines = [];
|
||||
this.on_change = null;
|
||||
|
||||
@ -246,12 +227,13 @@ class Text_document_class {
|
||||
* @param {object} meta - Metadata to associate with span
|
||||
*/
|
||||
insert_empty_span(line, character, meta) {
|
||||
let insertedSpan = null;
|
||||
const lineDef = this.lines[line];
|
||||
let newLine = [];
|
||||
let spanStartCharacter = 0;
|
||||
let wasInserted = false;
|
||||
for (let span of lineDef) {
|
||||
if (!wasInserted && character > spanStartCharacter && character <= spanStartCharacter + span.text.length) {
|
||||
if (!wasInserted && character >= spanStartCharacter && character <= spanStartCharacter + span.text.length) {
|
||||
let textBefore = span.text.slice(0, character - spanStartCharacter);
|
||||
let textAfter = span.text.slice(character - spanStartCharacter);
|
||||
if (textBefore.length > 0) {
|
||||
@ -264,10 +246,11 @@ class Text_document_class {
|
||||
for (let metaKey in meta) {
|
||||
newMeta[metaKey] = meta[metaKey];
|
||||
}
|
||||
newLine.push({
|
||||
insertedSpan = {
|
||||
text: '',
|
||||
meta: newMeta
|
||||
});
|
||||
};
|
||||
newLine.push(insertedSpan);
|
||||
if (textAfter.length > 0) {
|
||||
newLine.push({
|
||||
text: textAfter,
|
||||
@ -281,6 +264,7 @@ class Text_document_class {
|
||||
spanStartCharacter += span.text.length;
|
||||
}
|
||||
this.lines[line] = newLine;
|
||||
return insertedSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -291,25 +275,32 @@ class Text_document_class {
|
||||
*/
|
||||
insert_text(text, line, character) {
|
||||
|
||||
let insertedSpan;
|
||||
if (this.queuedMetaChanges) {
|
||||
this.insert_empty_span(line, character, this.queuedMetaChanges);
|
||||
insertedSpan = this.insert_empty_span(line, character, this.queuedMetaChanges);
|
||||
this.queuedMetaChanges = null;
|
||||
}
|
||||
|
||||
const insertLine = this.lines[line];
|
||||
const textHasNewline = text.includes('\n');
|
||||
let characterCount = 0;
|
||||
let characterCount = 0;
|
||||
let modifyingSpan = null;
|
||||
let previousSpans = [];
|
||||
let nextSpans = [];
|
||||
let modifyingSpan = null;
|
||||
let newLine = line;
|
||||
let newCharacter = character;
|
||||
|
||||
// Insert text into span at specified line/character
|
||||
for (let i = 0; i < insertLine.length; i++) {
|
||||
const span = insertLine[i];
|
||||
const spanLength = span.text.length;
|
||||
if (!modifyingSpan && (character > characterCount || character === 0) && character <= characterCount + spanLength) {
|
||||
const spanLength = span.text.length;
|
||||
if (span === insertedSpan) {
|
||||
console.log(
|
||||
(character > characterCount || character === 0),
|
||||
character <= characterCount + spanLength
|
||||
);
|
||||
}
|
||||
if (!modifyingSpan && (character > characterCount || character === 0) && character <= characterCount + spanLength) {
|
||||
if (insertLine[i + 1] && insertLine[i + 1].text === '') {
|
||||
modifyingSpan = insertLine[i + 1];
|
||||
} else {
|
||||
@ -651,7 +642,7 @@ class Text_document_class {
|
||||
}
|
||||
// Selection start splits the span it's inside of
|
||||
let choppedStartCharacters = 0;
|
||||
if (startCharacter > spanStartCharacter && startCharacter < spanStartCharacter + spanLength) {
|
||||
if (startCharacter > spanStartCharacter && startCharacter < spanStartCharacter + spanLength && lineIndex === startLine) {
|
||||
choppedStartCharacters = startCharacter - spanStartCharacter;
|
||||
newLine.push({
|
||||
text: span.text.slice(0, startCharacter - spanStartCharacter),
|
||||
@ -662,7 +653,7 @@ class Text_document_class {
|
||||
}
|
||||
newLine.push(span);
|
||||
// Selection end splits the span it's inside of
|
||||
if (endCharacter > spanStartCharacter && endCharacter < spanStartCharacter + spanLength) {
|
||||
if (endCharacter > spanStartCharacter && endCharacter < spanStartCharacter + spanLength && lineIndex === endLine) {
|
||||
newLine.push({
|
||||
text: span.text.slice(endCharacter - spanStartCharacter - choppedStartCharacters),
|
||||
meta: JSON.parse(JSON.stringify(span.meta))
|
||||
@ -1081,7 +1072,9 @@ class Text_editor_class {
|
||||
this.ctrlPressed = false;
|
||||
this.isMouseSelectionActive = false;
|
||||
this.mouseSelectionStartX = 0;
|
||||
this.mouseSelectionStartY = 0;
|
||||
this.mouseSelectionStartY = 0;
|
||||
this.mouseSelectionStartLine = null;
|
||||
this.mouseSelectionStartCharacter = null;
|
||||
this.mouseSelectionMoveX = null;
|
||||
this.mouseSelectionMoveY = null;
|
||||
this.mouseSelectionEdgeScrollInterval = null;
|
||||
@ -1122,13 +1115,20 @@ class Text_editor_class {
|
||||
return wrapText;
|
||||
}
|
||||
|
||||
get_span_font_metrics(span) {
|
||||
/**
|
||||
* Calculates font metrics for the given span and returns it. Caches by default.
|
||||
* @param {object} span - The span to calculate metrics for
|
||||
* @param {boolean} noCache - Skip caching if the metrics is expected to change in the future (e.g. font family not loaded yet.)
|
||||
*/
|
||||
get_span_font_metrics(span, noCache) {
|
||||
const fontSize = (span.meta.size || metaDefaults.size);
|
||||
const fontName = (span.meta.family || metaDefaults.family);
|
||||
let fontMetrics = fontMetricsMap.get(fontName + '_' + fontSize);
|
||||
if (!fontMetrics) {
|
||||
fontMetrics = new Font_metrics_class(fontName, fontSize);
|
||||
fontMetricsMap.set(fontName + '_' + fontSize, fontMetrics);
|
||||
if (!noCache) {
|
||||
fontMetricsMap.set(fontName + '_' + fontSize, fontMetrics);
|
||||
}
|
||||
}
|
||||
return fontMetrics;
|
||||
}
|
||||
@ -1174,9 +1174,10 @@ class Text_editor_class {
|
||||
trigger_cursor_start(layer, layerX, layerY) {
|
||||
this.isMouseSelectionActive = true;
|
||||
this.mouseSelectionStartX = layerX;
|
||||
this.mouseSelectionStartY = layerY;
|
||||
|
||||
this.mouseSelectionStartY = layerY;
|
||||
const cursorStart = this.get_cursor_position_from_absolute_position(layer, layerX, layerY);
|
||||
this.mouseSelectionStartLine = cursorStart.line;
|
||||
this.mouseSelectionStartCharacter = cursorStart.character;
|
||||
this.selection.set_position(cursorStart.line, cursorStart.character, false);
|
||||
}
|
||||
|
||||
@ -1186,6 +1187,7 @@ class Text_editor_class {
|
||||
this.mouseSelectionMoveX = layerX;
|
||||
this.mouseSelectionMoveY = layerY;
|
||||
const cursorEnd = this.get_cursor_position_from_absolute_position(layer, layerX, layerY);
|
||||
this.selection.set_position(this.mouseSelectionStartLine, this.mouseSelectionStartCharacter, false);
|
||||
this.selection.set_position(cursorEnd.line, cursorEnd.character, true);
|
||||
}
|
||||
}
|
||||
@ -1263,6 +1265,8 @@ class Text_editor_class {
|
||||
const boundary = layer.params.boundary;
|
||||
const textDirection = layer.params.text_direction;
|
||||
const wrapDirection = layer.params.wrap_direction;
|
||||
const halign = layer.params.halign;
|
||||
const valign = layer.params.valign;
|
||||
const isHorizontalTextDirection = ['ltr', 'rtl'].includes(textDirection);
|
||||
const isNegativeTextDirection = ['rtl', 'btt'].includes(textDirection);
|
||||
|
||||
@ -1283,17 +1287,18 @@ class Text_editor_class {
|
||||
let s = 0;
|
||||
for (s = 0; s < currentWrapSpans.length; s++) {
|
||||
const span = currentWrapSpans[s];
|
||||
const kerning = (span.meta.kerning || metaDefaults.kerning);
|
||||
const kerning = span.meta.kerning || metaDefaults.kerning;
|
||||
const family = span.meta.family || metaDefaults.family;
|
||||
let fontMetrics;
|
||||
if (isHorizontalTextDirection) {
|
||||
ctx.font =
|
||||
' ' + (span.meta.italic ? 'italic' : '') +
|
||||
' ' + (span.meta.bold ? 'bold' : '') +
|
||||
' ' + (span.meta.size || metaDefaults.size) + 'px' +
|
||||
' ' + (span.meta.family || metaDefaults.family);
|
||||
' ' + family;
|
||||
}
|
||||
else {
|
||||
fontMetrics = this.get_span_font_metrics(span);
|
||||
fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
|
||||
}
|
||||
for (let c = 0; c < span.text.length; c++) {
|
||||
const character = span.text[c];
|
||||
@ -1336,6 +1341,11 @@ class Text_editor_class {
|
||||
});
|
||||
}
|
||||
}
|
||||
// For word split only, break out.
|
||||
else if (layer.params.wrap === 'word') {
|
||||
wrapCharacterOffsets.push(wrapAccumulativeSize);
|
||||
break;
|
||||
}
|
||||
// Otherwise, split the word
|
||||
else {
|
||||
if (s === 0 && c === 0) {
|
||||
@ -1396,6 +1406,23 @@ class Text_editor_class {
|
||||
});
|
||||
}
|
||||
|
||||
// Adjust offsets for alignment along the text direction
|
||||
if ((isHorizontalTextDirection && halign !== 'left') || (!isHorizontalTextDirection && valign !== 'top')) {
|
||||
const maxTextDirectionSize = boundary === 'dynamic' ? totalTextDirectionSize : (isHorizontalTextDirection ? layer.width : layer.height);
|
||||
for (let line of lineRenderInfo.lines) {
|
||||
for (let wrap of line.wraps) {
|
||||
const isCentered = (isHorizontalTextDirection && halign == 'center') || (!isHorizontalTextDirection && valign === 'middle');
|
||||
const wrapSize = wrap.characterOffsets[wrap.characterOffsets.length - 1];
|
||||
const startOffset = (isCentered ? maxTextDirectionSize / 2 : maxTextDirectionSize) - (isCentered ? wrapSize / 2 : wrapSize);
|
||||
if (startOffset > 0) {
|
||||
for (let oi = 0; oi < wrap.characterOffsets.length; oi++) {
|
||||
wrap.characterOffsets[oi] += startOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the size of each line (e.g. line height if horizontal typing direction)
|
||||
let wrapSizeAccumulator = 0;
|
||||
let wrapCounter = 0;
|
||||
@ -1406,14 +1433,15 @@ class Text_editor_class {
|
||||
let wrapBaseline = 0;
|
||||
for (let span of wrap.spans) {
|
||||
let fontMetrics;
|
||||
const family = span.meta.family || metaDefaults.family;
|
||||
if (isHorizontalTextDirection) {
|
||||
fontMetrics = this.get_span_font_metrics(span);
|
||||
fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
|
||||
} else {
|
||||
ctx.font =
|
||||
' ' + (span.meta.italic ? 'italic' : '') +
|
||||
' ' + (span.meta.bold ? 'bold' : '') +
|
||||
' ' + (span.meta.size || metaDefaults.size) + 'px' +
|
||||
' ' + (span.meta.family || metaDefaults.family);
|
||||
' ' + family;
|
||||
}
|
||||
let spanWrapSize = isHorizontalTextDirection ? fontMetrics.height : ctx.measureText(character).width;
|
||||
let spanWrapBaseline = isHorizontalTextDirection ? fontMetrics.baseline : 0;
|
||||
@ -1437,7 +1465,7 @@ class Text_editor_class {
|
||||
}
|
||||
|
||||
render(ctx, layer) {
|
||||
if (this.hasValueChanged || layer.width != this.lastCalculatedLayerWidth || layer.height != this.lastCalculatedLayerHeight || !this.textBoundaryWidth || !this.textBoundaryHeight) {
|
||||
if (config.need_render_changed_params || this.hasValueChanged || layer.width != this.lastCalculatedLayerWidth || layer.height != this.lastCalculatedLayerHeight || !this.textBoundaryWidth || !this.textBoundaryHeight) {
|
||||
this.calculate_text_placement(ctx, layer);
|
||||
}
|
||||
|
||||
@ -1473,13 +1501,41 @@ class Text_editor_class {
|
||||
let characterIndex = 0;
|
||||
const characterOffsets = wrap.characterOffsets;
|
||||
for (let [spanIndex, span] of wrap.spans.entries()) {
|
||||
const kerning = (span.meta.kerning || metaDefaults.kerning);
|
||||
const kerning = span.meta.kerning != null ? span.meta.kerning : metaDefaults.kerning;
|
||||
const bold = span.meta.bold != null ? span.meta.bold : metaDefaults.bold;
|
||||
const italic = span.meta.italic != null ? span.meta.italic : metaDefaults.italic;
|
||||
const underline = span.meta.underline != null ? span.meta.underline : metaDefaults.underline;
|
||||
const strikethrough = span.meta.strikethrough != null ? span.meta.strikethrough : metaDefaults.strikethrough;
|
||||
const family = span.meta.family || metaDefaults.family;
|
||||
|
||||
if (fontLoadMap.get(family) == null) {
|
||||
fontLoadMap.set(family, false);
|
||||
WebFont.load({
|
||||
google: {
|
||||
families: [family]
|
||||
},
|
||||
fontactive: (family) => {
|
||||
fontLoadMap.set(family, true);
|
||||
this.hasValueChanged = true;
|
||||
this.Base_layers.render();
|
||||
},
|
||||
fontinactive: (family) => {
|
||||
alertify.error('Font ' + family + ' could not be loaded.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let fontMetrics;
|
||||
if (underline || strikethrough) {
|
||||
fontMetrics = this.get_span_font_metrics(span, !fontLoadMap.get(family));
|
||||
}
|
||||
|
||||
// Set styles for drawing
|
||||
ctx.font =
|
||||
' ' + (span.meta.italic ? 'italic' : '') +
|
||||
' ' + (span.meta.bold ? 'bold' : '') +
|
||||
' ' + (italic ? 'italic' : '') +
|
||||
' ' + (bold ? 'bold' : '') +
|
||||
' ' + Math.round(span.meta.size || metaDefaults.size) + 'px' +
|
||||
' ' + (span.meta.family || metaDefaults.family);
|
||||
' ' + family;
|
||||
const fill_color = span.meta.fill_color || metaDefaults.fill_color;
|
||||
let fillStyle;
|
||||
if (fill_color.startsWith('#')) {
|
||||
@ -1558,9 +1614,20 @@ class Text_editor_class {
|
||||
if (stroke_size) {
|
||||
ctx.strokeText(letter, letterDrawX, letterDrawY);
|
||||
}
|
||||
if (strikethrough) {
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.lineWidth = Math.max(1, fontMetrics.height / 20);
|
||||
ctx.fillRect(letterDrawX - 0.25, letterDrawY - (fontMetrics.height * .28), letterWidth + 0.5, ctx.lineWidth);
|
||||
}
|
||||
if (underline) {
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.lineWidth = Math.max(1, fontMetrics.height / 20);
|
||||
ctx.fillRect(letterDrawX - 0.25, letterDrawY + (ctx.lineWidth), letterWidth + 0.5, ctx.lineWidth);
|
||||
}
|
||||
characterIndex++;
|
||||
lineLetterCount++;
|
||||
}
|
||||
|
||||
if (span.text.length === 0) {
|
||||
if (cursorLine === lineIndex && cursorCharacter === lineLetterCount) {
|
||||
const lineStart = Math.round(drawOffsetTop + wrapSizes[wrapIndex].offset);
|
||||
@ -1652,6 +1719,7 @@ class Text_class extends Base_tools_class {
|
||||
|
||||
// Need a textarea in order to listen for keyboard inputs in an accessible, multi-platform independent way
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.id = 'text_tool_keyboard_input';
|
||||
this.textarea.setAttribute('autocorrect', 'off');
|
||||
this.textarea.setAttribute('autocapitalize', 'off');
|
||||
this.textarea.setAttribute('autocomplete', 'off');
|
||||
@ -1667,18 +1735,18 @@ class Text_class extends Base_tools_class {
|
||||
this.Base_layers.render();
|
||||
}, true);
|
||||
this.textarea.addEventListener('input', (e) => {
|
||||
if (this.layer) {
|
||||
const editor = this.get_editor(this.layer);
|
||||
if (config.layer) {
|
||||
const editor = this.get_editor(config.layer);
|
||||
editor.insert_text_at_current_position(e.target.value);
|
||||
e.target.value = '';
|
||||
this.Base_layers.render();
|
||||
this.extend_fixed_bounds(this.layer, editor);
|
||||
this.extend_fixed_bounds(config.layer, editor);
|
||||
}
|
||||
}, true);
|
||||
this.textarea.addEventListener('keydown', (e) => {
|
||||
if (this.layer) {
|
||||
if (config.layer) {
|
||||
let handled = true;
|
||||
const editor = this.get_editor(this.layer);
|
||||
const editor = this.get_editor(config.layer);
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
editor.delete_character_at_current_position(false);
|
||||
@ -1723,10 +1791,14 @@ class Text_class extends Base_tools_class {
|
||||
editor.selection.set_position(0, 0);
|
||||
const lastLine = editor.document.lines.length - 1;
|
||||
editor.selection.set_position(lastLine, editor.document.get_line_character_count(lastLine), true);
|
||||
} else {
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
case 'b':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
document.querySelector('#action_attributes #bold').click();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'c':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
@ -1735,10 +1807,20 @@ class Text_class extends Base_tools_class {
|
||||
this.textarea.setSelectionRange(0, 99999);
|
||||
document.execCommand('copy');
|
||||
this.textarea.value = '';
|
||||
} else {
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
case 'i':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
document.querySelector('#action_attributes #italic').click();
|
||||
break;
|
||||
}
|
||||
case 'u':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
document.querySelector('#action_attributes #underline').click();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'x':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
@ -1748,18 +1830,16 @@ class Text_class extends Base_tools_class {
|
||||
document.execCommand('copy');
|
||||
this.textarea.value = '';
|
||||
editor.delete_selection();
|
||||
} else {
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
if (handled) {
|
||||
this.update_tool_attributes(this.layer, editor);
|
||||
this.update_tool_attributes(config.layer, editor);
|
||||
this.Base_layers.render();
|
||||
}
|
||||
this.extend_fixed_bounds(this.layer, editor);
|
||||
this.extend_fixed_bounds(config.layer, editor);
|
||||
return !handled;
|
||||
}
|
||||
}, true);
|
||||
@ -1905,30 +1985,25 @@ class Text_class extends Base_tools_class {
|
||||
}
|
||||
const editor = this.get_editor(this.layer);
|
||||
|
||||
if (this.resizing) {
|
||||
this.resizing = false;
|
||||
}
|
||||
else if (this.creating) {
|
||||
if (this.creating) {
|
||||
let width = Math.abs(mouse.x - this.mousedownX);
|
||||
let height = Math.abs(mouse.y - this.mousedownY);
|
||||
|
||||
if (width == 0 && height == 0) {
|
||||
//same coordinates - cancel
|
||||
width = config.WIDTH - this.layer.x - Math.round(config.WIDTH / 50);
|
||||
height = 100;
|
||||
// Same coordinates - let render figure out dynamic width
|
||||
width = 1;
|
||||
height = 1;
|
||||
}
|
||||
//more data
|
||||
config.layer.x = Math.min(mouse.x, this.mousedownX);
|
||||
config.layer.y = Math.min(mouse.y, this.mousedownY);
|
||||
config.layer.width = width;
|
||||
config.layer.height = height;
|
||||
this.textarea.focus();
|
||||
this.creating = false;
|
||||
}
|
||||
else {
|
||||
else if (this.selecting) {
|
||||
editor.trigger_cursor_end();
|
||||
this.textarea.focus();
|
||||
this.selecting = false;
|
||||
|
||||
if (editor.selection.is_empty() && editor.document.queuedMetaChanges) {
|
||||
let meta = {};
|
||||
const existingMeta = editor.document.get_meta_range(editor.selection.start.line, editor.selection.start.character, editor.selection.end.line, editor.selection.end.character);
|
||||
@ -1944,6 +2019,19 @@ class Text_class extends Base_tools_class {
|
||||
// Resize layer based on text boundaries.
|
||||
this.extend_fixed_bounds(this.layer, editor);
|
||||
this.Base_layers.render();
|
||||
|
||||
// Center layer on mouse if not click & drag
|
||||
if (this.creating && config.layer.params.boundary === 'dynamic') {
|
||||
requestAnimationFrame(() => {
|
||||
config.layer.x -= config.layer.width / 2;
|
||||
config.layer.y -= config.layer.height / 2;
|
||||
this.Base_layers.render();
|
||||
});
|
||||
}
|
||||
|
||||
this.resizing = false;
|
||||
this.selecting = false;
|
||||
this.creating = false;
|
||||
}
|
||||
|
||||
|
||||
@ -1956,12 +2044,13 @@ class Text_class extends Base_tools_class {
|
||||
const wordEnd = editor.document.get_word_end_position(position.line, position.character, true);
|
||||
editor.selection.set_position(wordStart.line, wordStart.character);
|
||||
editor.selection.set_position(wordEnd.line, wordEnd.character, true);
|
||||
this.update_tool_attributes(this.layer, editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
on_params_update(param) {
|
||||
const editor = this.get_editor(this.layer);
|
||||
const editor = this.get_editor(config.layer);
|
||||
const value = param.value;
|
||||
const meta = {};
|
||||
switch (param.key) {
|
||||
@ -2021,8 +2110,8 @@ class Text_class extends Base_tools_class {
|
||||
toolAttributes.italic.value = meta.italic.includes(false) ? false : true;
|
||||
toolAttributes.underline.value = meta.underline.includes(false) ? false : true;
|
||||
toolAttributes.strikethrough.value = meta.strikethrough.includes(false) ? false : true;
|
||||
toolAttributes.fill = meta.fill_color.length === 1 ? meta.fill_color[0] : '#ffffff';
|
||||
toolAttributes.stroke = meta.stroke_color.length === 1 ? meta.stroke_color[0] : '#ffffff';
|
||||
toolAttributes.fill = meta.fill_color.length === 1 ? meta.fill_color[0] : '#000000';
|
||||
toolAttributes.stroke = meta.stroke_color.length === 1 ? meta.stroke_color[0] : '#000000';
|
||||
toolAttributes.stroke_size.value = meta.stroke_size.length === 1 ? meta.stroke_size[0] : parseFloat(null);
|
||||
toolAttributes.kerning.value = meta.kerning.length === 1 ? meta.kerning[0] : parseFloat(null);
|
||||
this.GUI_tools.show_action_attributes();
|
||||
@ -2075,78 +2164,7 @@ class Text_class extends Base_tools_class {
|
||||
this.selection.width = 0;
|
||||
this.selection.height = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
var font = params.family;
|
||||
if(typeof font == 'object'){
|
||||
font = font.value; //legacy
|
||||
}
|
||||
var text = params.text;
|
||||
var size = params.size;
|
||||
var line_height = size;
|
||||
|
||||
if(text == undefined){
|
||||
//not defined yet
|
||||
return;
|
||||
}
|
||||
|
||||
this.load_fonts();
|
||||
|
||||
//set styles
|
||||
if (params.bold && params.italic)
|
||||
ctx.font = "Bold Italic " + size + "px " + font;
|
||||
else if (params.bold)
|
||||
ctx.font = "Bold " + size + "px " + font;
|
||||
else if (params.italic)
|
||||
ctx.font = "Italic " + size + "px " + font;
|
||||
else
|
||||
ctx.font = "Normal " + size + "px " + font;
|
||||
ctx.fillStyle = layer.color;
|
||||
ctx.strokeStyle = layer.color;
|
||||
ctx.lineWidth = params.stroke_size;
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
var paragraphs = text.split("\n");
|
||||
var offset_y = -line_height;
|
||||
for(var i in paragraphs){
|
||||
var block_test = paragraphs[i];
|
||||
var lines = this.getLines(ctx, block_test, layer.width);
|
||||
for (var j in lines) {
|
||||
offset_y += line_height;
|
||||
this.render_text_line(ctx, layer, lines[j], offset_y);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
render_text_line(ctx, layer, text, offset_y) {
|
||||
var params = layer.params;
|
||||
var stroke = params.stroke;
|
||||
var align = params.align;
|
||||
if(typeof align == 'object'){
|
||||
align = align.value; //legacy
|
||||
}
|
||||
align = align.toLowerCase();
|
||||
var text_width = ctx.measureText(text).width;
|
||||
|
||||
//tabs
|
||||
text = text.replace(/\t/g, ' ');
|
||||
|
||||
var start_x = layer.x;
|
||||
if (align == 'right') {
|
||||
start_x = layer.x + layer.width - text_width;
|
||||
}
|
||||
else if (align == 'center') {
|
||||
start_x = layer.x + Math.round(layer.width / 2) - Math.round(text_width / 2);
|
||||
}
|
||||
|
||||
if (stroke == false)
|
||||
ctx.fillText(text, start_x, layer.y + offset_y);
|
||||
else
|
||||
ctx.strokeText(text, start_x, layer.y + offset_y);
|
||||
}
|
||||
*/
|
||||
|
||||
get_editor(layer) {
|
||||
let editor = layerEditors.get(layer);
|
||||
@ -2174,6 +2192,12 @@ class Text_class extends Base_tools_class {
|
||||
}
|
||||
]);
|
||||
}
|
||||
params.boundary = 'box';
|
||||
params.halign = params.align ? params.align.toLowerCase() : 'left';
|
||||
params.valign = 'top';
|
||||
params.text_direction = 'ltr';
|
||||
params.wrap_direction = 'ttb';
|
||||
params.wrap = 'word';
|
||||
delete params.text;
|
||||
delete params.family;
|
||||
delete params.size;
|
||||
@ -2181,13 +2205,8 @@ class Text_class extends Base_tools_class {
|
||||
delete params.italic;
|
||||
delete params.stroke;
|
||||
delete params.stroke_size;
|
||||
delete params.align;
|
||||
layer.data = lines;
|
||||
params.boundary = 'box';
|
||||
params.halign = 'left';
|
||||
params.valign = 'top';
|
||||
params.text_direction = 'ltr';
|
||||
params.wrap_direction = 'ttb';
|
||||
params.wrap = 'word';
|
||||
}
|
||||
|
||||
// Create initial layer data if new layer
|
||||
@ -2196,7 +2215,7 @@ class Text_class extends Base_tools_class {
|
||||
layer.data = [[{
|
||||
text: '',
|
||||
meta: {
|
||||
family: params.font.value !== metaDefaults.family ? params.font.value : undefined,
|
||||
family: params.font.value !== metaDefaults.family && params.font.value ? params.font.value : undefined,
|
||||
size: params.size !== metaDefaults.size && !isNaN(params.size) ? params.size : undefined,
|
||||
bold: params.bold.value !== metaDefaults.bold ? params.bold.value : undefined,
|
||||
italic: params.italic.value !== metaDefaults.italic ? params.italic.value : undefined,
|
||||
@ -2211,87 +2230,12 @@ class Text_class extends Base_tools_class {
|
||||
}
|
||||
|
||||
editor.set_lines(layer.data);
|
||||
editor.Base_layers = this.Base_layers;
|
||||
editor.layer = layer;
|
||||
layerEditors.set(layer, editor);
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
|
||||
load_fonts(){
|
||||
if(this.is_fonts_loaded == true){
|
||||
return;
|
||||
}
|
||||
|
||||
var fonts = this.get_external_fonts();
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
for(var i in fonts) {
|
||||
var font_family = fonts[i].replace(/[^a-zA-Z0-9 ]/g, '').replace(/ +/g, '+');
|
||||
var font_url = 'https://fonts.googleapis.com/css?family=' + font_family;
|
||||
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = font_url;
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
this.is_fonts_loaded = true;
|
||||
}
|
||||
|
||||
get_fonts(){
|
||||
var default_fonts = [
|
||||
"Arial",
|
||||
"Courier",
|
||||
"Impact",
|
||||
"Helvetica",
|
||||
"Monospace",
|
||||
"Tahoma",
|
||||
"Times New Roman",
|
||||
"Verdana",
|
||||
];
|
||||
|
||||
var external_fonts = this.get_external_fonts();
|
||||
|
||||
//merge and sort
|
||||
var merged = default_fonts.concat(external_fonts);
|
||||
merged = merged.sort();
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
get_external_fonts(){
|
||||
var google_fonts = [
|
||||
"Amatic SC",
|
||||
"Arimo",
|
||||
"Codystar",
|
||||
"Creepster",
|
||||
"Indie Flower",
|
||||
"Lato",
|
||||
"Lora",
|
||||
"Merriweather",
|
||||
"Monoton",
|
||||
"Montserrat",
|
||||
"Mukta",
|
||||
"Muli",
|
||||
"Nosifer",
|
||||
"Nunito",
|
||||
"Oswald",
|
||||
"Orbitron",
|
||||
"Pacifico",
|
||||
"PT Sans",
|
||||
"PT Serif",
|
||||
"Playfair Display",
|
||||
"Poppins",
|
||||
"Raleway",
|
||||
"Roboto",
|
||||
"Rubik",
|
||||
"Special Elite",
|
||||
"Tangerine",
|
||||
"Titillium Web",
|
||||
"Ubuntu",
|
||||
];
|
||||
|
||||
return google_fonts;
|
||||
}
|
||||
|
||||
get_text_layer_at_mouse(e) {
|
||||
const layers_sorted = this.Base_layers.get_sorted_layers();
|
||||
@ -2310,7 +2254,7 @@ class Text_class extends Base_tools_class {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default Text_class;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user