Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +7 -203
- visualization.py +132 -26
app.py
CHANGED
|
@@ -1032,210 +1032,14 @@ def main():
|
|
| 1032 |
import traceback
|
| 1033 |
st.code(traceback.format_exc())
|
| 1034 |
|
| 1035 |
-
#
|
| 1036 |
st.markdown("---")
|
| 1037 |
-
st.
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
with col2:
|
| 1045 |
-
if st.button("🖼️ Generate Annotated Image", use_container_width=True, type="primary"):
|
| 1046 |
-
with st.spinner("Generating annotated image..."):
|
| 1047 |
-
try:
|
| 1048 |
-
from visualization import extract_window_pdb, transform_pdb_string
|
| 1049 |
-
|
| 1050 |
-
# Create a simple structure visualization using matplotlib
|
| 1051 |
-
# Since we can't capture py3Dmol directly, we'll create a matplotlib-based view
|
| 1052 |
-
|
| 1053 |
-
# Extract structures
|
| 1054 |
-
ref_pdb = extract_window_pdb(
|
| 1055 |
-
selected_row['Ref_Path'],
|
| 1056 |
-
selected_row['Ref_Window']
|
| 1057 |
-
)
|
| 1058 |
-
|
| 1059 |
-
query_pdb = extract_window_pdb(
|
| 1060 |
-
selected_row['Query_Path'],
|
| 1061 |
-
selected_row['Query_Window']
|
| 1062 |
-
)
|
| 1063 |
-
|
| 1064 |
-
query_aligned_pdb = transform_pdb_string(
|
| 1065 |
-
query_pdb,
|
| 1066 |
-
selected_row['Rotation_Matrix'],
|
| 1067 |
-
selected_row['Query_COM'],
|
| 1068 |
-
selected_row['Ref_COM']
|
| 1069 |
-
)
|
| 1070 |
-
|
| 1071 |
-
# Parse coordinates for visualization
|
| 1072 |
-
from rmsd_utils import parse_residue_atoms
|
| 1073 |
-
|
| 1074 |
-
# Create a matplotlib-based 3D visualization
|
| 1075 |
-
import matplotlib.pyplot as plt
|
| 1076 |
-
from mpl_toolkits.mplot3d import Axes3D
|
| 1077 |
-
|
| 1078 |
-
fig = plt.figure(figsize=(12, 9), dpi=150)
|
| 1079 |
-
ax = fig.add_subplot(111, projection='3d')
|
| 1080 |
-
|
| 1081 |
-
# Function to extract coordinates from PDB string
|
| 1082 |
-
def get_coords_from_pdb_string(pdb_string):
|
| 1083 |
-
coords = []
|
| 1084 |
-
for line in pdb_string.split('\n'):
|
| 1085 |
-
if line.startswith(('ATOM', 'HETATM')):
|
| 1086 |
-
try:
|
| 1087 |
-
x = float(line[30:38].strip())
|
| 1088 |
-
y = float(line[38:46].strip())
|
| 1089 |
-
z = float(line[46:54].strip())
|
| 1090 |
-
atom_name = line[12:16].strip()
|
| 1091 |
-
coords.append((x, y, z, atom_name))
|
| 1092 |
-
except:
|
| 1093 |
-
continue
|
| 1094 |
-
return coords
|
| 1095 |
-
|
| 1096 |
-
# Get coordinates
|
| 1097 |
-
ref_coords = get_coords_from_pdb_string(ref_pdb)
|
| 1098 |
-
query_coords = get_coords_from_pdb_string(query_aligned_pdb)
|
| 1099 |
-
|
| 1100 |
-
# Plot reference structure (blue)
|
| 1101 |
-
if ref_coords:
|
| 1102 |
-
ref_x = [c[0] for c in ref_coords]
|
| 1103 |
-
ref_y = [c[1] for c in ref_coords]
|
| 1104 |
-
ref_z = [c[2] for c in ref_coords]
|
| 1105 |
-
ax.scatter(ref_x, ref_y, ref_z, c='#4A90E2', s=40, alpha=0.8, label='Reference')
|
| 1106 |
-
|
| 1107 |
-
# Connect backbone atoms
|
| 1108 |
-
backbone_atoms = ['P', "C4'", "C3'", "O3'"]
|
| 1109 |
-
ref_backbone = [(c[0], c[1], c[2]) for c in ref_coords if c[3] in backbone_atoms]
|
| 1110 |
-
if len(ref_backbone) > 1:
|
| 1111 |
-
bb_x = [c[0] for c in ref_backbone]
|
| 1112 |
-
bb_y = [c[1] for c in ref_backbone]
|
| 1113 |
-
bb_z = [c[2] for c in ref_backbone]
|
| 1114 |
-
ax.plot(bb_x, bb_y, bb_z, c='#4A90E2', linewidth=2, alpha=0.6)
|
| 1115 |
-
|
| 1116 |
-
# Plot query structure (red)
|
| 1117 |
-
if query_coords:
|
| 1118 |
-
query_x = [c[0] for c in query_coords]
|
| 1119 |
-
query_y = [c[1] for c in query_coords]
|
| 1120 |
-
query_z = [c[2] for c in query_coords]
|
| 1121 |
-
ax.scatter(query_x, query_y, query_z, c='#E94B3C', s=40, alpha=0.8, label='Query (Aligned)')
|
| 1122 |
-
|
| 1123 |
-
# Connect backbone atoms
|
| 1124 |
-
query_backbone = [(c[0], c[1], c[2]) for c in query_coords if c[3] in backbone_atoms]
|
| 1125 |
-
if len(query_backbone) > 1:
|
| 1126 |
-
bb_x = [c[0] for c in query_backbone]
|
| 1127 |
-
bb_y = [c[1] for c in query_backbone]
|
| 1128 |
-
bb_z = [c[2] for c in query_backbone]
|
| 1129 |
-
ax.plot(bb_x, bb_y, bb_z, c='#E94B3C', linewidth=2, alpha=0.6)
|
| 1130 |
-
|
| 1131 |
-
# Set labels and title
|
| 1132 |
-
ax.set_xlabel('X (Å)', fontsize=10)
|
| 1133 |
-
ax.set_ylabel('Y (Å)', fontsize=10)
|
| 1134 |
-
ax.set_zlabel('Z (Å)', fontsize=10)
|
| 1135 |
-
ax.legend(fontsize=10, loc='upper right')
|
| 1136 |
-
|
| 1137 |
-
# Set viewing angle
|
| 1138 |
-
ax.view_init(elev=20, azim=45)
|
| 1139 |
-
|
| 1140 |
-
# Equal aspect ratio
|
| 1141 |
-
if ref_coords or query_coords:
|
| 1142 |
-
all_coords = ref_coords + query_coords
|
| 1143 |
-
all_x = [c[0] for c in all_coords]
|
| 1144 |
-
all_y = [c[1] for c in all_coords]
|
| 1145 |
-
all_z = [c[2] for c in all_coords]
|
| 1146 |
-
|
| 1147 |
-
max_range = max(
|
| 1148 |
-
max(all_x) - min(all_x),
|
| 1149 |
-
max(all_y) - min(all_y),
|
| 1150 |
-
max(all_z) - min(all_z)
|
| 1151 |
-
) / 2.0
|
| 1152 |
-
|
| 1153 |
-
mid_x = (max(all_x) + min(all_x)) / 2
|
| 1154 |
-
mid_y = (max(all_y) + min(all_y)) / 2
|
| 1155 |
-
mid_z = (max(all_z) + min(all_z)) / 2
|
| 1156 |
-
|
| 1157 |
-
ax.set_xlim(mid_x - max_range, mid_x + max_range)
|
| 1158 |
-
ax.set_ylim(mid_y - max_range, mid_y + max_range)
|
| 1159 |
-
ax.set_zlim(mid_z - max_range, mid_z + max_range)
|
| 1160 |
-
|
| 1161 |
-
plt.tight_layout()
|
| 1162 |
-
|
| 1163 |
-
# Save to temporary buffer
|
| 1164 |
-
buf = io.BytesIO()
|
| 1165 |
-
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white')
|
| 1166 |
-
plt.close()
|
| 1167 |
-
buf.seek(0)
|
| 1168 |
-
|
| 1169 |
-
# Now annotate this image
|
| 1170 |
-
annotated_img = annotate_alignment_image(
|
| 1171 |
-
image_data=buf.read(),
|
| 1172 |
-
rmsd=selected_row['RMSD'],
|
| 1173 |
-
ref_name=selected_row['Reference'],
|
| 1174 |
-
query_name=selected_row['Query'],
|
| 1175 |
-
ref_sequence=selected_row['Ref_Sequence'],
|
| 1176 |
-
query_sequence=selected_row['Query_Sequence'],
|
| 1177 |
-
output_format='JPEG'
|
| 1178 |
-
)
|
| 1179 |
-
|
| 1180 |
-
# Generate filename
|
| 1181 |
-
ref_clean = selected_row['Reference'].replace('.pdb', '')
|
| 1182 |
-
query_clean = selected_row['Query'].replace('.pdb', '')
|
| 1183 |
-
filename = f"annotated_{ref_clean}_{query_clean}_RMSD_{selected_row['RMSD']:.3f}.jpg"
|
| 1184 |
-
|
| 1185 |
-
# Show preview and download button
|
| 1186 |
-
st.success("✅ Annotated image generated!")
|
| 1187 |
-
st.image(annotated_img, caption="Annotated Structure Alignment", use_column_width=True)
|
| 1188 |
-
|
| 1189 |
-
st.download_button(
|
| 1190 |
-
label="📥 Download Annotated JPEG",
|
| 1191 |
-
data=annotated_img.getvalue(),
|
| 1192 |
-
file_name=filename,
|
| 1193 |
-
mime="image/jpeg",
|
| 1194 |
-
use_container_width=True,
|
| 1195 |
-
help="Download JPEG with RMSD and sequence information"
|
| 1196 |
-
)
|
| 1197 |
-
|
| 1198 |
-
except Exception as e:
|
| 1199 |
-
st.error(f"Error generating annotated image: {str(e)}")
|
| 1200 |
-
import traceback
|
| 1201 |
-
st.code(traceback.format_exc())
|
| 1202 |
-
|
| 1203 |
-
# Fallback: offer the upload option
|
| 1204 |
-
st.info("💡 Alternatively, you can download a screenshot from the 3D viewer above using the '📷 Download PNG' button, then upload it below:")
|
| 1205 |
-
|
| 1206 |
-
uploaded_screenshot = st.file_uploader(
|
| 1207 |
-
"Upload screenshot (PNG/JPG)",
|
| 1208 |
-
type=['png', 'jpg', 'jpeg'],
|
| 1209 |
-
key=f"screenshot_upload_fallback_{selected_viz_idx}"
|
| 1210 |
-
)
|
| 1211 |
-
|
| 1212 |
-
if uploaded_screenshot is not None:
|
| 1213 |
-
try:
|
| 1214 |
-
annotated_img = annotate_alignment_image(
|
| 1215 |
-
image_data=uploaded_screenshot.read(),
|
| 1216 |
-
rmsd=selected_row['RMSD'],
|
| 1217 |
-
ref_name=selected_row['Reference'],
|
| 1218 |
-
query_name=selected_row['Query'],
|
| 1219 |
-
ref_sequence=selected_row['Ref_Sequence'],
|
| 1220 |
-
query_sequence=selected_row['Query_Sequence'],
|
| 1221 |
-
output_format='JPEG'
|
| 1222 |
-
)
|
| 1223 |
-
|
| 1224 |
-
st.image(annotated_img, use_column_width=True)
|
| 1225 |
-
|
| 1226 |
-
ref_clean = selected_row['Reference'].replace('.pdb', '')
|
| 1227 |
-
query_clean = selected_row['Query'].replace('.pdb', '')
|
| 1228 |
-
filename = f"annotated_{ref_clean}_{query_clean}_RMSD_{selected_row['RMSD']:.3f}.jpg"
|
| 1229 |
-
|
| 1230 |
-
st.download_button(
|
| 1231 |
-
label="📥 Download Annotated JPEG",
|
| 1232 |
-
data=annotated_img.getvalue(),
|
| 1233 |
-
file_name=filename,
|
| 1234 |
-
mime="image/jpeg",
|
| 1235 |
-
use_container_width=True
|
| 1236 |
-
)
|
| 1237 |
-
except Exception as e2:
|
| 1238 |
-
st.error(f"Error annotating uploaded image: {str(e2)}")
|
| 1239 |
|
| 1240 |
# Show transformation details
|
| 1241 |
with st.expander("🔧 Transformation Details"):
|
|
|
|
| 1032 |
import traceback
|
| 1033 |
st.code(traceback.format_exc())
|
| 1034 |
|
| 1035 |
+
# Automatic Annotation Info
|
| 1036 |
st.markdown("---")
|
| 1037 |
+
st.success("✅ **Automatic Annotation:** When you click 'Download PNG' in the 3D viewer above, the image automatically includes RMSD, structure names, and sequences!")
|
| 1038 |
+
st.info("💡 **Customize font size:** Use the 'Annotation Font Size' dropdown in the viewer controls (top-right) to choose from Small, Medium, Large (default), or Extra Large fonts!")
|
| 1039 |
+
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
|
| 1044 |
# Show transformation details
|
| 1045 |
with st.expander("🔧 Transformation Details"):
|
visualization.py
CHANGED
|
@@ -315,6 +315,16 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
|
|
| 315 |
<option value="gray">Gray</option>
|
| 316 |
</select>
|
| 317 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
</div>
|
| 319 |
|
| 320 |
<div class="rmsd-info">
|
|
@@ -607,14 +617,41 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
|
|
| 607 |
var refName = "{ref_name}".replace('.pdb', '');
|
| 608 |
var queryName = "{query_name}".replace('.pdb', '');
|
| 609 |
var rmsdValue = "{f'{rmsd:.3f}' if rmsd is not None else 'NA'}";
|
| 610 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
// Ensure viewer is rendered
|
| 613 |
if (viewer) {{
|
| 614 |
viewer.render();
|
| 615 |
}}
|
| 616 |
|
| 617 |
-
// Get the container element
|
| 618 |
const container = document.getElementById('container');
|
| 619 |
|
| 620 |
if (!container) {{
|
|
@@ -630,34 +667,103 @@ def create_structure_visualization(ref_path, query_path, ref_window_indices, que
|
|
| 630 |
useCORS: true,
|
| 631 |
allowTaint: true
|
| 632 |
}}).then(function(canvas) {{
|
| 633 |
-
//
|
| 634 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
|
| 641 |
-
// Trigger download
|
| 642 |
-
document.body.appendChild(link);
|
| 643 |
-
link.click();
|
| 644 |
-
document.body.removeChild(link);
|
| 645 |
}}).catch(function(error) {{
|
| 646 |
console.error('html2canvas error:', error);
|
| 647 |
-
|
| 648 |
-
// Fallback: just capture the canvas without overlays
|
| 649 |
-
const canvas = document.querySelector('#container canvas');
|
| 650 |
-
if (canvas) {{
|
| 651 |
-
const dataURL = canvas.toDataURL('image/png');
|
| 652 |
-
const link = document.createElement('a');
|
| 653 |
-
link.download = filename;
|
| 654 |
-
link.href = dataURL;
|
| 655 |
-
document.body.appendChild(link);
|
| 656 |
-
link.click();
|
| 657 |
-
document.body.removeChild(link);
|
| 658 |
-
}} else {{
|
| 659 |
-
alert('Download failed. Please try again.');
|
| 660 |
-
}}
|
| 661 |
}});
|
| 662 |
|
| 663 |
}} catch (error) {{
|
|
|
|
| 315 |
<option value="gray">Gray</option>
|
| 316 |
</select>
|
| 317 |
</div>
|
| 318 |
+
|
| 319 |
+
<div class="control-section">
|
| 320 |
+
<div class="section-title">Annotation Font Size</div>
|
| 321 |
+
<select id="annotationFontSize">
|
| 322 |
+
<option value="small">Small (18pt/16pt/14pt)</option>
|
| 323 |
+
<option value="medium">Medium (22pt/18pt/16pt)</option>
|
| 324 |
+
<option value="large" selected>Large (28pt/22pt/18pt)</option>
|
| 325 |
+
<option value="xlarge">Extra Large (36pt/28pt/22pt)</option>
|
| 326 |
+
</select>
|
| 327 |
+
</div>
|
| 328 |
</div>
|
| 329 |
|
| 330 |
<div class="rmsd-info">
|
|
|
|
| 617 |
var refName = "{ref_name}".replace('.pdb', '');
|
| 618 |
var queryName = "{query_name}".replace('.pdb', '');
|
| 619 |
var rmsdValue = "{f'{rmsd:.3f}' if rmsd is not None else 'NA'}";
|
| 620 |
+
var refSeq = "{ref_sequence if ref_sequence else ''}";
|
| 621 |
+
var querySeq = "{query_sequence if query_sequence else ''}";
|
| 622 |
+
|
| 623 |
+
var filenameOriginal = 'alignment_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
|
| 624 |
+
var filenameAnnotated = 'annotated_' + refName + '_' + queryName + '_RMSD_' + rmsdValue + '.png';
|
| 625 |
+
|
| 626 |
+
// Get selected font size
|
| 627 |
+
const fontSizeSelect = document.getElementById('annotationFontSize');
|
| 628 |
+
const fontSizeOption = fontSizeSelect ? fontSizeSelect.value : 'large';
|
| 629 |
+
|
| 630 |
+
// Define font sizes based on selection (all values are at 2x scale for high resolution)
|
| 631 |
+
let fontSizes;
|
| 632 |
+
switch(fontSizeOption) {{
|
| 633 |
+
case 'small':
|
| 634 |
+
fontSizes = {{ rmsd: 36, name: 32, seq: 28 }}; // 18pt/16pt/14pt at 2x
|
| 635 |
+
break;
|
| 636 |
+
case 'medium':
|
| 637 |
+
fontSizes = {{ rmsd: 44, name: 36, seq: 32 }}; // 22pt/18pt/16pt at 2x
|
| 638 |
+
break;
|
| 639 |
+
case 'large':
|
| 640 |
+
fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // 28pt/22pt/18pt at 2x
|
| 641 |
+
break;
|
| 642 |
+
case 'xlarge':
|
| 643 |
+
fontSizes = {{ rmsd: 72, name: 56, seq: 44 }}; // 36pt/28pt/22pt at 2x
|
| 644 |
+
break;
|
| 645 |
+
default:
|
| 646 |
+
fontSizes = {{ rmsd: 56, name: 44, seq: 36 }}; // Default to large
|
| 647 |
+
}}
|
| 648 |
|
| 649 |
// Ensure viewer is rendered
|
| 650 |
if (viewer) {{
|
| 651 |
viewer.render();
|
| 652 |
}}
|
| 653 |
|
| 654 |
+
// Get the container element
|
| 655 |
const container = document.getElementById('container');
|
| 656 |
|
| 657 |
if (!container) {{
|
|
|
|
| 667 |
useCORS: true,
|
| 668 |
allowTaint: true
|
| 669 |
}}).then(function(canvas) {{
|
| 670 |
+
// Create ANNOTATED version
|
| 671 |
+
const annotatedCanvas = document.createElement('canvas');
|
| 672 |
+
annotatedCanvas.width = canvas.width;
|
| 673 |
+
annotatedCanvas.height = canvas.height;
|
| 674 |
+
const ctx = annotatedCanvas.getContext('2d');
|
| 675 |
+
|
| 676 |
+
// Draw the original image onto new canvas
|
| 677 |
+
ctx.drawImage(canvas, 0, 0);
|
| 678 |
+
|
| 679 |
+
// Add annotations
|
| 680 |
+
const margin = 30; // Scaled for 2x resolution
|
| 681 |
+
const padding = 24;
|
| 682 |
+
const lineSpacing = 16;
|
| 683 |
+
|
| 684 |
+
// Prepare annotation text with selected font sizes
|
| 685 |
+
const annotations = [
|
| 686 |
+
{{ text: 'RMSD: ' + rmsdValue + ' Å', fontSize: fontSizes.rmsd, fontFamily: 'bold Arial', color: '#E94B3C' }},
|
| 687 |
+
{{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
|
| 688 |
+
{{ text: 'Reference: ' + refName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
|
| 689 |
+
{{ text: ' Seq: ' + refSeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }},
|
| 690 |
+
{{ text: '', fontSize: 20, fontFamily: 'Arial', color: '#333' }}, // Spacer
|
| 691 |
+
{{ text: 'Query: ' + queryName, fontSize: fontSizes.name, fontFamily: 'Arial', color: '#333' }},
|
| 692 |
+
{{ text: ' Seq: ' + querySeq, fontSize: fontSizes.seq, fontFamily: 'Courier New, monospace', color: '#666' }}
|
| 693 |
+
];
|
| 694 |
+
|
| 695 |
+
// Calculate box dimensions
|
| 696 |
+
let maxWidth = 0;
|
| 697 |
+
let totalHeight = padding * 2;
|
| 698 |
+
const textMetrics = [];
|
| 699 |
|
| 700 |
+
annotations.forEach(ann => {{
|
| 701 |
+
if (ann.text) {{
|
| 702 |
+
ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
|
| 703 |
+
const metrics = ctx.measureText(ann.text);
|
| 704 |
+
const height = ann.fontSize * 1.2; // Approximate height
|
| 705 |
+
textMetrics.push({{ width: metrics.width, height: height }});
|
| 706 |
+
maxWidth = Math.max(maxWidth, metrics.width);
|
| 707 |
+
totalHeight += height + lineSpacing;
|
| 708 |
+
}} else {{
|
| 709 |
+
textMetrics.push({{ width: 0, height: lineSpacing / 2 }});
|
| 710 |
+
totalHeight += lineSpacing / 2;
|
| 711 |
+
}}
|
| 712 |
+
}});
|
| 713 |
+
|
| 714 |
+
const boxWidth = maxWidth + padding * 2;
|
| 715 |
+
const boxHeight = totalHeight;
|
| 716 |
+
|
| 717 |
+
// Position box in bottom-left
|
| 718 |
+
const boxX = margin;
|
| 719 |
+
const boxY = annotatedCanvas.height - boxHeight - margin;
|
| 720 |
+
|
| 721 |
+
// Draw semi-transparent white background with rounded corners
|
| 722 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
| 723 |
+
const radius = 16;
|
| 724 |
+
ctx.beginPath();
|
| 725 |
+
ctx.moveTo(boxX + radius, boxY);
|
| 726 |
+
ctx.lineTo(boxX + boxWidth - radius, boxY);
|
| 727 |
+
ctx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius);
|
| 728 |
+
ctx.lineTo(boxX + boxWidth, boxY + boxHeight - radius);
|
| 729 |
+
ctx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight);
|
| 730 |
+
ctx.lineTo(boxX + radius, boxY + boxHeight);
|
| 731 |
+
ctx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius);
|
| 732 |
+
ctx.lineTo(boxX, boxY + radius);
|
| 733 |
+
ctx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY);
|
| 734 |
+
ctx.closePath();
|
| 735 |
+
ctx.fill();
|
| 736 |
+
|
| 737 |
+
// Draw border
|
| 738 |
+
ctx.strokeStyle = 'rgba(200, 200, 200, 0.95)';
|
| 739 |
+
ctx.lineWidth = 2;
|
| 740 |
+
ctx.stroke();
|
| 741 |
+
|
| 742 |
+
// Draw text
|
| 743 |
+
let currentY = boxY + padding;
|
| 744 |
+
annotations.forEach((ann, idx) => {{
|
| 745 |
+
if (ann.text) {{
|
| 746 |
+
ctx.font = ann.fontSize + 'px ' + ann.fontFamily;
|
| 747 |
+
ctx.fillStyle = ann.color;
|
| 748 |
+
ctx.fillText(ann.text, boxX + padding, currentY + textMetrics[idx].height * 0.8);
|
| 749 |
+
currentY += textMetrics[idx].height + lineSpacing;
|
| 750 |
+
}} else {{
|
| 751 |
+
currentY += textMetrics[idx].height;
|
| 752 |
+
}}
|
| 753 |
+
}});
|
| 754 |
+
|
| 755 |
+
// Download ONLY the annotated PNG
|
| 756 |
+
const annotatedDataURL = annotatedCanvas.toDataURL('image/png');
|
| 757 |
+
const linkAnnotated = document.createElement('a');
|
| 758 |
+
linkAnnotated.download = filenameAnnotated;
|
| 759 |
+
linkAnnotated.href = annotatedDataURL;
|
| 760 |
+
document.body.appendChild(linkAnnotated);
|
| 761 |
+
linkAnnotated.click();
|
| 762 |
+
document.body.removeChild(linkAnnotated);
|
| 763 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
}}).catch(function(error) {{
|
| 765 |
console.error('html2canvas error:', error);
|
| 766 |
+
alert('Error creating images. Please try again.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
}});
|
| 768 |
|
| 769 |
}} catch (error) {{
|