@datagrok/peptides 0.8.9 → 0.8.13
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.
- package/.eslintrc.json +2 -1
- package/dist/package-test.js +22626 -0
- package/dist/package.js +21429 -0
- package/dist/vendors-node_modules_datagrok-libraries_ml_src_workers_dimensionality-reducer_js.js +8840 -0
- package/jest.config.js +33 -0
- package/package.json +75 -62
- package/src/__jest__/remote.test.ts +50 -0
- package/src/__jest__/test-node.ts +96 -0
- package/src/model.ts +950 -86
- package/src/monomer-library.ts +8 -0
- package/src/package-test.ts +3 -2
- package/src/package.ts +57 -22
- package/src/peptides.ts +165 -119
- package/src/styles.css +8 -0
- package/src/tests/peptides-tests.ts +17 -78
- package/src/tests/utils.ts +1 -7
- package/src/utils/SAR-multiple-filter.ts +439 -0
- package/src/utils/SAR-multiple-selection.ts +177 -0
- package/src/utils/cell-renderer.ts +49 -50
- package/src/utils/chem-palette.ts +61 -163
- package/src/utils/constants.ts +56 -0
- package/src/utils/filtering-statistics.ts +62 -0
- package/src/utils/multiple-sequence-alignment.ts +33 -2
- package/src/utils/multivariate-analysis.ts +79 -0
- package/src/utils/peptide-similarity-space.ts +12 -31
- package/src/utils/types.ts +10 -0
- package/src/viewers/logo-viewer.ts +2 -1
- package/src/viewers/peptide-space-viewer.ts +121 -0
- package/src/viewers/sar-viewer.ts +111 -313
- package/src/viewers/stacked-barchart-viewer.ts +126 -173
- package/src/widgets/analyze-peptides.ts +39 -18
- package/src/widgets/distribution.ts +61 -0
- package/src/widgets/manual-alignment.ts +3 -3
- package/src/widgets/peptide-molecule.ts +4 -4
- package/src/widgets/subst-table.ts +30 -22
- package/test-Peptides-f8114def7953-4bf59d70.html +256 -0
- package/src/describe.ts +0 -534
- package/src/utils/split-aligned.ts +0 -72
- package/src/viewers/subst-viewer.ts +0 -320
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
|
|
3
|
+
import {StringDictionary} from '@datagrok-libraries/utils/src/type-declarations';
|
|
4
|
+
|
|
5
|
+
type Operation = (op1: boolean, op2: boolean) => boolean;
|
|
6
|
+
|
|
7
|
+
/** Logical operations awailable between selection items. */
|
|
8
|
+
const Operations: {[op: string]: Operation} = {
|
|
9
|
+
and: (op1: boolean, op2: boolean) => op1 && op2,
|
|
10
|
+
or: (op1: boolean, op2: boolean) => op1 || op2,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type FilterOperation = 'and' | 'or';
|
|
14
|
+
type PositionFilter = {[pos: string]: Set<string>};
|
|
15
|
+
|
|
16
|
+
/** Implements multiple selection in position-residue space. */
|
|
17
|
+
export class MultipleSelection {
|
|
18
|
+
conjunction: boolean;
|
|
19
|
+
filter: PositionFilter;
|
|
20
|
+
protected operation: Operation;
|
|
21
|
+
protected complete: (v: boolean) => boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an instance of MultipleSelection.
|
|
25
|
+
* @param {FilterOperation} [operation='and'] Operation to apply to items.
|
|
26
|
+
*/
|
|
27
|
+
constructor(operation: FilterOperation = 'and') {
|
|
28
|
+
this.conjunction = operation == 'and';
|
|
29
|
+
this.filter = {};
|
|
30
|
+
this.operation = Operations[operation];
|
|
31
|
+
this.complete = (v: boolean) => (this.conjunction && !v) || (!this.conjunction && v);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Adds position-residue entity into selection.
|
|
36
|
+
* @param {string} pos Position in a sequence.
|
|
37
|
+
* @param {string} res Residue at the position.
|
|
38
|
+
*/
|
|
39
|
+
input(pos: string, res: string) {
|
|
40
|
+
if (!this.filter[pos])
|
|
41
|
+
this.filter[pos] = new Set([]);
|
|
42
|
+
|
|
43
|
+
if (this.filter[pos].has(res))
|
|
44
|
+
this.remove(pos, res);
|
|
45
|
+
else
|
|
46
|
+
this.filter[pos].add(res);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Removes position-residue entity from selection.
|
|
51
|
+
* @param {string} pos Position in a sequence.
|
|
52
|
+
* @param {string} res Residue at the position.
|
|
53
|
+
*/
|
|
54
|
+
remove(pos: string, res: string) {
|
|
55
|
+
if (this.filter[pos]) {
|
|
56
|
+
this.filter[pos].delete(res);
|
|
57
|
+
|
|
58
|
+
if (this.filter[pos].size == 0)
|
|
59
|
+
delete this.filter[pos];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sets the particular residue at position into selection.
|
|
65
|
+
* @param {string} pos Position in a sequence.
|
|
66
|
+
* @param {string} res Residue at the position.
|
|
67
|
+
*/
|
|
68
|
+
set(pos: string, res: string) {
|
|
69
|
+
for (const p of Object.keys(this.filter))
|
|
70
|
+
delete this.filter[p];
|
|
71
|
+
|
|
72
|
+
this.filter[pos] = new Set([res]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sets the particular position with the given residues into selection.
|
|
77
|
+
* @param {string} pos Position in a sequence.
|
|
78
|
+
* @param {string[]} values Residues list at the position.
|
|
79
|
+
*/
|
|
80
|
+
setPos(pos: string, values: string[]) {
|
|
81
|
+
this.filter[pos] = new Set(values);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets the particular residue to be at given positions into selection.
|
|
86
|
+
* @param {string} res Residue to set.
|
|
87
|
+
* @param {string[]} values Positions list to assign the residue to.
|
|
88
|
+
*/
|
|
89
|
+
setRes(res: string, values: string[]) {
|
|
90
|
+
for (const pos of values) {
|
|
91
|
+
if (this.filter[pos])
|
|
92
|
+
this.filter[pos].add(res);
|
|
93
|
+
else
|
|
94
|
+
this.filter[pos] = new Set([res]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Evaluates selection into a list of booleans of the same length as the given data frame.
|
|
100
|
+
* @param {DG.DataFrame} df Data frame to consider.
|
|
101
|
+
* @param {StringDictionary} [mapper={}] Optional residues mapper.
|
|
102
|
+
* @return {boolean[]} List of trues/falses corresponding selection constructed.
|
|
103
|
+
*/
|
|
104
|
+
eval(df: DG.DataFrame, mapper: StringDictionary = {}): boolean[] {
|
|
105
|
+
const itemsCount = df.rowCount;
|
|
106
|
+
const cond = new Array<boolean>(itemsCount).fill(this.conjunction);
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < itemsCount; ++i)
|
|
109
|
+
cond[i] = this.match(i, df, mapper);
|
|
110
|
+
|
|
111
|
+
return cond;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tests if i-th element matches filter.
|
|
116
|
+
* @param {number} i Element's index
|
|
117
|
+
* @param {DG.DataFrame} df Data frame to consider.
|
|
118
|
+
* @param {StringDictionary} [mapper={}] Optional residues mapper.
|
|
119
|
+
* @return {boolean} Result of the test.
|
|
120
|
+
*/
|
|
121
|
+
match(i: number, df: DG.DataFrame, mapper: StringDictionary = {}): boolean {
|
|
122
|
+
let cond: boolean = this.conjunction;
|
|
123
|
+
|
|
124
|
+
for (const [posColumnName, resFilter] of Object.entries(this.filter)) {
|
|
125
|
+
const residue = df.get(posColumnName, i);
|
|
126
|
+
const isMatched = resFilter.has(mapper[residue] ?? residue);
|
|
127
|
+
cond = this.operation(cond, isMatched);
|
|
128
|
+
|
|
129
|
+
if (this.complete(cond))
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return cond;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Tests if position-residue is selected.
|
|
137
|
+
* @param {string} pos Position in a sequence.
|
|
138
|
+
* @param {string} res Residue at the position.
|
|
139
|
+
* @param {StringDictionary} [mapper={}] Optional residues mapper.
|
|
140
|
+
* @return {boolean} True if this entity is selected else false.
|
|
141
|
+
*/
|
|
142
|
+
test(pos: string, res: string, mapper: StringDictionary = {}): boolean {
|
|
143
|
+
if (this.filter[pos])
|
|
144
|
+
return this.filter[pos].has(mapper[res] ?? res);
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Make string representation of the selected items.
|
|
151
|
+
* @return {string} String representation.
|
|
152
|
+
*/
|
|
153
|
+
toString(): string {
|
|
154
|
+
let repr = '';
|
|
155
|
+
|
|
156
|
+
for (const [pos, res] of Object.entries(this.filter))
|
|
157
|
+
repr += `${pos}: ${Array.from(res).join(',')}\n`;
|
|
158
|
+
|
|
159
|
+
return repr;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Turns filter into grouping query string.
|
|
164
|
+
* @param {string} [residueColumnName='Res'] Optional residues column name.
|
|
165
|
+
* @return {string} Query-formatted string.
|
|
166
|
+
*/
|
|
167
|
+
toQuery(residueColumnName: string = 'Res'): string {
|
|
168
|
+
const alt: string[] = [];
|
|
169
|
+
|
|
170
|
+
for (const [pos, residues] of Object.entries(this.filter)) {
|
|
171
|
+
for (const res of residues)
|
|
172
|
+
alt.push(`${residueColumnName} = ${res} and Pos = ${pos}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return alt.join(' or ');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {ChemPalette} from './chem-palette';
|
|
2
2
|
import * as DG from 'datagrok-api/dg';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import * as C from './constants';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A function to expand column size based on its contents.
|
|
@@ -40,8 +40,8 @@ export function expandColumn(
|
|
|
40
40
|
* @param {boolean} [grouping=false] Is grouping enabled.
|
|
41
41
|
*/
|
|
42
42
|
export function setAARRenderer(col: DG.Column, grid: DG.Grid | null = null, grouping = false) {
|
|
43
|
-
col.semType =
|
|
44
|
-
col.setTag('cell.renderer',
|
|
43
|
+
col.semType = C.SEM_TYPES.AMINO_ACIDS;
|
|
44
|
+
col.setTag('cell.renderer', C.SEM_TYPES.AMINO_ACIDS);
|
|
45
45
|
if (grouping)
|
|
46
46
|
col.setTag('groups', `${grouping}`);
|
|
47
47
|
|
|
@@ -89,42 +89,35 @@ function printLeftOrCentered(
|
|
|
89
89
|
|
|
90
90
|
if (colorPart.length >= 3) {
|
|
91
91
|
if (colorPart.substring(0, 3) in ChemPalette.AAFullNames)
|
|
92
|
-
colorPart = ChemPalette.AAFullNames[s.substring(0, 3)] + colorPart.
|
|
92
|
+
colorPart = ChemPalette.AAFullNames[s.substring(0, 3)] + colorPart.substring(3);
|
|
93
93
|
else if (colorPart.substring(1, 4) in ChemPalette.AAFullNames)
|
|
94
|
-
colorPart = colorPart[0] + ChemPalette.AAFullNames[s.substring(1, 4)] + colorPart.
|
|
94
|
+
colorPart = colorPart[0] + ChemPalette.AAFullNames[s.substring(1, 4)] + colorPart.substring(4);
|
|
95
95
|
}
|
|
96
|
-
let grayPart = pivot == -1 ? '' : s.
|
|
96
|
+
let grayPart = pivot == -1 ? '' : s.substring(pivot);
|
|
97
97
|
if (hideMod) {
|
|
98
98
|
let end = colorPart.lastIndexOf(')');
|
|
99
99
|
let beg = colorPart.indexOf('(');
|
|
100
100
|
if (beg > -1 && end > -1 && end - beg > 2)
|
|
101
|
-
colorPart = colorPart.
|
|
101
|
+
colorPart = colorPart.substring(0, beg) + '(+)' + colorPart.substring(end + 1);
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
end = grayPart.lastIndexOf(')');
|
|
105
105
|
beg = grayPart.indexOf('(');
|
|
106
106
|
if (beg > -1 && end > -1 && end - beg > 2)
|
|
107
|
-
grayPart = grayPart.
|
|
107
|
+
grayPart = grayPart.substring(0, beg) + '(+)' + grayPart.substring(end + 1);
|
|
108
108
|
}
|
|
109
109
|
const textSize = g.measureText(colorPart + grayPart);
|
|
110
110
|
const indent = 5;
|
|
111
111
|
|
|
112
112
|
const colorTextSize = g.measureText(colorPart);
|
|
113
|
+
const dy = (textSize.fontBoundingBoxAscent + textSize.fontBoundingBoxDescent) / 2;
|
|
113
114
|
|
|
114
115
|
function draw(dx1: number, dx2: number) {
|
|
115
116
|
g.fillStyle = color;
|
|
116
117
|
g.globalAlpha = transparencyRate;
|
|
117
|
-
g.fillText(
|
|
118
|
-
colorPart,
|
|
119
|
-
x + dx1,
|
|
120
|
-
y + (textSize.fontBoundingBoxAscent + textSize.fontBoundingBoxDescent) / 2,
|
|
121
|
-
);
|
|
118
|
+
g.fillText(colorPart, x + dx1, y + dy);
|
|
122
119
|
g.fillStyle = ChemPalette.undefinedColor;
|
|
123
|
-
g.fillText(
|
|
124
|
-
grayPart,
|
|
125
|
-
x + dx2,
|
|
126
|
-
y + (textSize.fontBoundingBoxAscent + textSize.fontBoundingBoxDescent) / 2,
|
|
127
|
-
);
|
|
120
|
+
g.fillText(grayPart, x + dx2, y + dy);
|
|
128
121
|
}
|
|
129
122
|
|
|
130
123
|
|
|
@@ -132,8 +125,9 @@ function printLeftOrCentered(
|
|
|
132
125
|
draw(indent, indent + colorTextSize.width);
|
|
133
126
|
return x + colorTextSize.width + g.measureText(grayPart).width;
|
|
134
127
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
const dx = (w - textSize.width) / 2;
|
|
129
|
+
draw(dx, dx + colorTextSize.width);
|
|
130
|
+
return x + dx + colorTextSize.width;
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
133
|
|
|
@@ -164,7 +158,7 @@ export class AminoAcidsCellRenderer extends DG.GridCellRenderer {
|
|
|
164
158
|
* @memberof AminoAcidsCellRenderer
|
|
165
159
|
*/
|
|
166
160
|
get cellType() {
|
|
167
|
-
return
|
|
161
|
+
return C.SEM_TYPES.AMINO_ACIDS;
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
/**
|
|
@@ -209,7 +203,7 @@ export class AminoAcidsCellRenderer extends DG.GridCellRenderer {
|
|
|
209
203
|
render(
|
|
210
204
|
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, gridCell: DG.GridCell,
|
|
211
205
|
cellStyle: DG.GridCellStyle) {
|
|
212
|
-
this.chemPalette ??= new ChemPalette('grok', gridCell.tableColumn?.getTag('groups') ? true : false);
|
|
206
|
+
// this.chemPalette ??= new ChemPalette('grok', gridCell.tableColumn?.getTag('groups') ? true : false);
|
|
213
207
|
|
|
214
208
|
y -= 2;
|
|
215
209
|
g.save();
|
|
@@ -219,7 +213,7 @@ export class AminoAcidsCellRenderer extends DG.GridCellRenderer {
|
|
|
219
213
|
g.font = `12px monospace`;
|
|
220
214
|
g.textBaseline = 'top';
|
|
221
215
|
const s: string = gridCell.cell.value ? gridCell.cell.value : '-';
|
|
222
|
-
let [color, outerS, innerS, pivot] =
|
|
216
|
+
let [color, outerS, innerS, pivot] = ChemPalette.getColorAAPivot(s);
|
|
223
217
|
if (innerS)
|
|
224
218
|
outerS = s;
|
|
225
219
|
|
|
@@ -253,7 +247,7 @@ export class AlignedSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
253
247
|
* @memberof AlignedSequenceCellRenderer
|
|
254
248
|
*/
|
|
255
249
|
get cellType() {
|
|
256
|
-
return
|
|
250
|
+
return C.SEM_TYPES.ALIGNED_SEQUENCE;
|
|
257
251
|
}
|
|
258
252
|
|
|
259
253
|
/**
|
|
@@ -289,10 +283,10 @@ export class AlignedSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
289
283
|
* @memberof AlignedSequenceCellRenderer
|
|
290
284
|
*/
|
|
291
285
|
render(
|
|
292
|
-
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
|
|
293
|
-
|
|
286
|
+
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, gridCell: DG.GridCell,
|
|
287
|
+
cellStyle: DG.GridCellStyle,
|
|
294
288
|
) {
|
|
295
|
-
const grid = gridCell.
|
|
289
|
+
const grid = gridCell.grid;
|
|
296
290
|
const cell = gridCell.cell;
|
|
297
291
|
w = grid ? Math.min(grid.canvas.width - x, w) : g.canvas.width - x;
|
|
298
292
|
g.save();
|
|
@@ -307,16 +301,16 @@ export class AlignedSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
307
301
|
const subParts = s.split('-');
|
|
308
302
|
const [text, simplified] = processSequence(subParts);
|
|
309
303
|
const textSize = g.measureText(text.join(''));
|
|
310
|
-
|
|
304
|
+
let x1 = Math.max(x, x + (w - textSize.width) / 2);
|
|
311
305
|
|
|
312
|
-
subParts.forEach((amino
|
|
313
|
-
let [color, outerAmino,, pivot] =
|
|
306
|
+
subParts.forEach((amino, index) => {
|
|
307
|
+
let [color, outerAmino,, pivot] = ChemPalette.getColorAAPivot(amino);
|
|
314
308
|
g.fillStyle = ChemPalette.undefinedColor;
|
|
315
309
|
if (index + 1 < subParts.length) {
|
|
316
310
|
const gap = simplified ? '' : ' ';
|
|
317
|
-
outerAmino += `${outerAmino?'':'-'}${gap}`;
|
|
311
|
+
outerAmino += `${outerAmino ? '' : '-'}${gap}`;
|
|
318
312
|
}
|
|
319
|
-
|
|
313
|
+
x1 = printLeftOrCentered(x1, y, w, h, g, outerAmino, color, pivot, true);
|
|
320
314
|
});
|
|
321
315
|
|
|
322
316
|
g.restore();
|
|
@@ -408,10 +402,9 @@ export class AlignedSequenceDifferenceCellRenderer extends DG.GridCellRenderer {
|
|
|
408
402
|
* @memberof AlignedSequenceDifferenceCellRenderer
|
|
409
403
|
*/
|
|
410
404
|
render(
|
|
411
|
-
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const grid = gridCell.dart.grid ? gridCell.grid : gridCell.dart.grid;
|
|
405
|
+
g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, gridCell: DG.GridCell,
|
|
406
|
+
cellStyle: DG.GridCellStyle) {
|
|
407
|
+
const grid = gridCell.grid;
|
|
415
408
|
const cell = gridCell.cell;
|
|
416
409
|
|
|
417
410
|
w = grid ? Math.min(grid.canvas.width - x, w) : g.canvas.width - x;
|
|
@@ -429,26 +422,32 @@ export class AlignedSequenceDifferenceCellRenderer extends DG.GridCellRenderer {
|
|
|
429
422
|
const subParts2 = s2.split('-');
|
|
430
423
|
const [text] = processSequence(subParts1);
|
|
431
424
|
const textSize = g.measureText(text.join(''));
|
|
432
|
-
|
|
425
|
+
let updatedX = Math.max(x, x + (w - (textSize.width + subParts1.length * 4)) / 2);
|
|
426
|
+
// 28 is the height of the two substitutions on top of each other + space
|
|
427
|
+
const updatedY = Math.max(y, y + (h - 28) / 2);
|
|
433
428
|
|
|
429
|
+
let amino2;
|
|
430
|
+
let updatedAmino1: string;
|
|
431
|
+
let updatedAmino2: string;
|
|
434
432
|
subParts1.forEach((amino1: string, index) => {
|
|
435
|
-
|
|
436
|
-
const [color1, amino1Outer, amino1Inner, pivot1] =
|
|
437
|
-
const [color2, amino2Outer, amino2Inner, pivot2] =
|
|
433
|
+
amino2 = subParts2[index];
|
|
434
|
+
const [color1, amino1Outer, amino1Inner, pivot1] = ChemPalette.getColorAAPivot(amino1);
|
|
435
|
+
const [color2, amino2Outer, amino2Inner, pivot2] = ChemPalette.getColorAAPivot(amino2);
|
|
438
436
|
|
|
439
|
-
|
|
440
|
-
|
|
437
|
+
updatedAmino1 = amino1Outer + (amino1Inner !== '' ? '(' + amino1Inner + ')' : '');
|
|
438
|
+
updatedAmino1 = updatedAmino1 === '' ? '-' : updatedAmino1;
|
|
441
439
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
amino2 = amino2 === '' ? '-' : amino2;
|
|
440
|
+
if (amino1 != amino2) {
|
|
441
|
+
updatedAmino2 = amino2Outer + (amino2Inner !== '' ? '(' + amino2Inner + ')' : '');
|
|
442
|
+
updatedAmino2 = updatedAmino2 === '' ? '-' : updatedAmino2;
|
|
446
443
|
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
const vShift = 7;
|
|
445
|
+
const subX0 = printLeftOrCentered(updatedX, updatedY - vShift, w, h, g, updatedAmino1, color1, pivot1, true);
|
|
446
|
+
const subX1 = printLeftOrCentered(updatedX, updatedY + vShift, w, h, g, updatedAmino2, color2, pivot2, true);
|
|
447
|
+
updatedX = Math.max(subX1, subX0);
|
|
450
448
|
} else
|
|
451
|
-
|
|
449
|
+
updatedX = printLeftOrCentered(updatedX, updatedY, w, h, g, updatedAmino1, color1, pivot1, true, true, 0.5);
|
|
450
|
+
updatedX += 4;
|
|
452
451
|
});
|
|
453
452
|
g.restore();
|
|
454
453
|
}
|