|
2 | 2 | <html lang="en">
|
3 | 3 | <head>
|
4 | 4 | <meta charset="UTF-8">
|
5 |
| - <title>blobSketch version 1.11</title> |
| 5 | + <title>blobSketch version 1.12</title> |
6 | 6 | <style>
|
7 | 7 | /* ───────── 1. Base / Reset ───────── */
|
8 | 8 | body {
|
|
475 | 475 | box-shadow:0 0 8px 3px #ffe600 !important;
|
476 | 476 | }
|
477 | 477 |
|
| 478 | +/* --- prettier Presets gallery ---------------------------------- */ |
| 479 | +.presets-gallery{ |
| 480 | + display:grid; |
| 481 | + grid-template-columns: repeat(auto-fill, minmax(120px,1fr)); |
| 482 | + gap:12px; |
| 483 | + padding:4px; |
| 484 | + max-height:60vh; /* scroll if many presets */ |
| 485 | + overflow-y:auto; |
| 486 | +} |
| 487 | + |
| 488 | +.preset-card{ |
| 489 | + aspect-ratio:1/1; /* always square */ |
| 490 | + background:#fafafa; |
| 491 | + border:2px solid #000; |
| 492 | + box-shadow:3px 3px 0 #000; |
| 493 | + position:relative; |
| 494 | + transition:transform .15s, box-shadow .15s; |
| 495 | +} |
| 496 | +.preset-card:hover{ |
| 497 | + transform:translateY(-3px); |
| 498 | + box-shadow:5px 5px 0 #000; |
| 499 | +} |
| 500 | +.preset-card:active{ transform:translateY(0); } |
| 501 | + |
| 502 | +.preset-card img, |
| 503 | +.preset-card svg{ width:100%; height:100%; object-fit:cover; display:block; } |
| 504 | + |
| 505 | +.preset-card figcaption{ |
| 506 | + position:absolute; inset:0; |
| 507 | + background:rgba(0,0,0,.58); |
| 508 | + color:#fff; font:12px "Courier New",monospace; |
| 509 | + display:flex; align-items:center; justify-content:center; |
| 510 | + text-align:center; padding:4px; |
| 511 | + opacity:0; transition:opacity .15s; |
| 512 | +} |
| 513 | +.preset-card:hover figcaption{ opacity:1; } |
| 514 | + |
| 515 | +/* highlight the card the user just applied */ |
| 516 | +.preset-card.card-selected{ |
| 517 | + outline:3px solid #ffe600; |
| 518 | + box-shadow:0 0 10px 3px #ffe600; |
| 519 | + transform:none; |
| 520 | +} |
| 521 | + |
478 | 522 | </style>
|
479 | 523 | </head>
|
480 | 524 | <body>
|
|
511 | 555 | <span id="exportFilledJPGOption">...Fill>JPG</span>
|
512 | 556 | </div>
|
513 | 557 | </span>
|
| 558 | + <span id="menuPresets">Presets</span> |
514 | 559 | <span id="menuAbout">About</span>
|
515 | 560 | <span id="menuHelp">Help</span>
|
516 | 561 | </div>
|
@@ -655,10 +700,113 @@ <h4>Canvas</h4>
|
655 | 700 | </div>
|
656 | 701 | </div>
|
657 | 702 |
|
| 703 | + <!-- ░ PRESETS DIALOG ░ --> |
| 704 | + <div class="dialog-backdrop" id="presetsBackdrop"> |
| 705 | + <div class="dialog-content presets-dialog"> |
| 706 | + <h3>Select a Preset</h3> |
| 707 | + |
| 708 | + <div id="presetsGallery" class="presets-gallery"> |
| 709 | + <!-- Grid preset card --> |
| 710 | + <div class="preset-card" id="presetGrid"> |
| 711 | + <svg viewBox="0 0 100 100" width="100%" height="100%"> |
| 712 | + <rect width="100" height="100" fill="#fff"/> |
| 713 | + <g stroke="#000" stroke-width="2" fill="none"> |
| 714 | + <!-- 5 × 5 grid preview --> |
| 715 | + <circle cx="10" cy="10" r="8"/> |
| 716 | + <circle cx="30" cy="10" r="8"/> |
| 717 | + <circle cx="50" cy="10" r="8"/> |
| 718 | + <circle cx="70" cy="10" r="8"/> |
| 719 | + <circle cx="90" cy="10" r="8"/> |
| 720 | + |
| 721 | + <circle cx="10" cy="30" r="8"/> |
| 722 | + <circle cx="30" cy="30" r="8"/> |
| 723 | + <circle cx="50" cy="30" r="8"/> |
| 724 | + <circle cx="70" cy="30" r="8"/> |
| 725 | + <circle cx="90" cy="30" r="8"/> |
| 726 | + |
| 727 | + <circle cx="10" cy="50" r="8"/> |
| 728 | + <circle cx="30" cy="50" r="8"/> |
| 729 | + <circle cx="50" cy="50" r="8"/> |
| 730 | + <circle cx="70" cy="50" r="8"/> |
| 731 | + <circle cx="90" cy="50" r="8"/> |
| 732 | + |
| 733 | + <circle cx="10" cy="70" r="8"/> |
| 734 | + <circle cx="30" cy="70" r="8"/> |
| 735 | + <circle cx="50" cy="70" r="8"/> |
| 736 | + <circle cx="70" cy="70" r="8"/> |
| 737 | + <circle cx="90" cy="70" r="8"/> |
| 738 | + |
| 739 | + <circle cx="10" cy="90" r="8"/> |
| 740 | + <circle cx="30" cy="90" r="8"/> |
| 741 | + <circle cx="50" cy="90" r="8"/> |
| 742 | + <circle cx="70" cy="90" r="8"/> |
| 743 | + <circle cx="90" cy="90" r="8"/> |
| 744 | + </g> |
| 745 | + </svg> |
| 746 | + <figcaption>Grid of Circles</figcaption> |
| 747 | + </div> |
| 748 | + <div class="preset-card" id="presetBlobGrid"> |
| 749 | + <img src="images/grid_of_blobs.png" alt="Grid of Blobs"> |
| 750 | + <figcaption>Grid of Blobs</figcaption> |
| 751 | +</div> |
| 752 | + </div> |
| 753 | + |
| 754 | + <button id="closePresets">Close</button> |
| 755 | + </div> |
| 756 | + </div> |
| 757 | + |
| 758 | + <div class="dialog-backdrop" id="gridBackdrop"> |
| 759 | + <div class="dialog-content"> |
| 760 | + <h3>Grid of Circles</h3> |
| 761 | + |
| 762 | + <label>Count |
| 763 | + <input type="number" id="gridCount" value="5" min="1" max="12"> |
| 764 | + </label><br> |
| 765 | + |
| 766 | + <label> |
| 767 | + <input type="checkbox" id="autoRadius" checked> |
| 768 | + Fit circles to canvas |
| 769 | + </label><br> |
| 770 | + |
| 771 | + <div id="radiusRow" style="display:none;"> |
| 772 | + <label>Radius |
| 773 | + <input type="number" id="gridRadius" value="20" min="2" max="200"> |
| 774 | + </label><br> |
| 775 | + </div> |
| 776 | + |
| 777 | + <button id="applyGrid">Apply</button> |
| 778 | + <button id="cancelGrid">Cancel</button> |
| 779 | + </div> |
| 780 | + |
| 781 | +</div> |
| 782 | +<div class="dialog-backdrop" id="blobGridBackdrop"> |
| 783 | + <div class="dialog-content"> |
| 784 | + <h3>Grid of Blobs</h3> |
| 785 | + |
| 786 | + <label>Count (rows = cols) |
| 787 | + <input type="number" id="blobGridCount" value="5" min="1" max="10"> |
| 788 | + </label><br> |
| 789 | + |
| 790 | + <label> |
| 791 | + <input type="checkbox" id="blobAutoSize" checked> |
| 792 | + Fit blobs to canvas |
| 793 | + </label><br> |
| 794 | + |
| 795 | + <div id="blobSizeRow" style="display:none;"> |
| 796 | + <label>Base Radius |
| 797 | + <input type="number" id="blobBaseRadius" value="20" min="4" max="120"> |
| 798 | + </label><br> |
| 799 | + </div> |
| 800 | + |
| 801 | + <button id="applyBlobGrid">Apply</button> |
| 802 | + <button id="cancelBlobGrid">Cancel</button> |
| 803 | + </div> |
| 804 | + </div> |
| 805 | + |
658 | 806 | <!-- Modals/Dialogs: About and Help -->
|
659 | 807 | <div class="dialog-backdrop" id="aboutBackdrop">
|
660 | 808 | <div class="dialog-content">
|
661 |
| - <p>blobSketch version 1.11</p> |
| 809 | + <p>blobSketch version 1.12</p> |
662 | 810 | <p>by Colin Reid</p>
|
663 | 811 | <button id="closeAbout">OK</button>
|
664 | 812 | </div>
|
@@ -799,6 +947,174 @@ <h3>Import Options</h3>
|
799 | 947 | setupSubmenu("menuFile", "fileSubmenu");
|
800 | 948 | setupSubmenu("menuExport", "exportSubmenu");
|
801 | 949 |
|
| 950 | + |
| 951 | + /* ---------- PRESETS ---------- */ |
| 952 | + $("menuPresets").onclick = () => { |
| 953 | + $("presetsBackdrop").style.display = "flex"; |
| 954 | + }; |
| 955 | + |
| 956 | + /* close gallery */ |
| 957 | + $("closePresets").onclick = () => |
| 958 | + $("presetsBackdrop").style.display = "none"; |
| 959 | + |
| 960 | + /* show the Grid-options dialog when the card is clicked */ |
| 961 | + $("presetGrid").onclick = () => { |
| 962 | + $("presetsBackdrop").style.display = "none"; |
| 963 | + $("gridBackdrop").style.display = "flex"; |
| 964 | + }; |
| 965 | + |
| 966 | + /* cancel grid dialog */ |
| 967 | + $("cancelGrid").onclick = () => |
| 968 | + $("gridBackdrop").style.display = "none"; |
| 969 | + |
| 970 | + /* show / hide the custom-radius row */ |
| 971 | + $("autoRadius").onchange = () => |
| 972 | + $("radiusRow").style.display = $("autoRadius").checked ? "none" : "block"; |
| 973 | + |
| 974 | + /* apply grid */ |
| 975 | + $("applyGrid").onclick = () => { |
| 976 | + const count = Math.min(12, |
| 977 | + Math.max(1, parseInt($("gridCount").value, 10) || 1)); |
| 978 | + /* this radius would make circles kiss each other and the edges */ |
| 979 | + const maxR = Math.min(canvas.width, canvas.height) / (count * 2); |
| 980 | + |
| 981 | + /* the actual radius – either maxR or a user-supplied smaller one */ |
| 982 | + const radius = $("autoRadius").checked |
| 983 | + ? maxR |
| 984 | + : Math.min(parseFloat($("gridRadius").value) || 2, maxR); |
| 985 | + |
| 986 | + /* IMPORTANT: spacing is based on maxR so the centres never move */ |
| 987 | + const spacing = 2 * maxR; |
| 988 | + |
| 989 | + for (let r = 0; r < count; r++) { |
| 990 | + for (let c = 0; c < count; c++) { |
| 991 | + circles.push({ |
| 992 | + x: maxR + c * spacing, |
| 993 | + y: maxR + r * spacing, |
| 994 | + radius // may be < maxR ⇒ nice margin |
| 995 | + }); |
| 996 | + history.push({ type: "circle" }); |
| 997 | + } |
| 998 | + } |
| 999 | + |
| 1000 | + /* set Blob Size slider to 0 % and Excitability to 85 % */ |
| 1001 | + setDotScale(0); // updates UI + dotScaleFactor |
| 1002 | + damping = 0.85; |
| 1003 | + $("dampingSlider").value = 85; |
| 1004 | + $("dampingValue").textContent = "85%"; |
| 1005 | + /* ── 1. High Repulsion ON ── */ |
| 1006 | + if (interRepelMult !== 3) { // 1 = normal, 3 = high |
| 1007 | + interRepelMult = 3; |
| 1008 | + toggleActiveIcon($("iconDiffusion"), true); |
| 1009 | + } |
| 1010 | + |
| 1011 | + /* ── 2. Blob draw-mode ── */ |
| 1012 | + setDrawMode("blob"); // switches UI + cursor |
| 1013 | + |
| 1014 | + /* ── 3. Edit tools OFF ── */ |
| 1015 | + slicingMode = dragMode = pinMode = |
| 1016 | + freezeMode = deleteMode = false; // clear flags |
| 1017 | + $("editToolsSection").classList.remove("edit-on"); |
| 1018 | + ["iconSlice","iconDrag","iconPin","iconFreeze","iconDelete"] |
| 1019 | + .forEach(id=>setIconActive($(id),false,"","")); |
| 1020 | + updateCanvasCursor(); |
| 1021 | + |
| 1022 | + if (!solidFillMode) { |
| 1023 | + solidFillMode = true; |
| 1024 | + $("iconFill").title = "Solid Fill On"; |
| 1025 | + toggleActiveIcon($("iconFill"), true); |
| 1026 | + } |
| 1027 | + |
| 1028 | + toast(`Grid (${count}×${count}) added`, 1600); |
| 1029 | + $("gridBackdrop").style.display = "none"; |
| 1030 | + }; |
| 1031 | + |
| 1032 | + |
| 1033 | + /* === GRID-OF-BLOBS PRESET ======================================== */ |
| 1034 | + $("presetBlobGrid").onclick = () => { |
| 1035 | + $("presetsBackdrop").style.display = "none"; // ✨ hide gallery |
| 1036 | + $("blobGridBackdrop").style.display = "flex"; // show blob-dialog |
| 1037 | + }; |
| 1038 | + |
| 1039 | + /* toggle custom size row */ |
| 1040 | + $("blobAutoSize").onchange = () => |
| 1041 | + $("blobSizeRow").style.display = $("blobAutoSize").checked ? "none" : "block"; |
| 1042 | + |
| 1043 | + /* helper to create a simple circular blob (you could swap in fancier math) */ |
| 1044 | + function makeCircleBlob(cx, cy, r, colorArr) { |
| 1045 | + const steps = 48; |
| 1046 | + const pts = []; |
| 1047 | + for (let i = 0; i < steps; i++) { |
| 1048 | + const θ = i / steps * 2 * Math.PI; |
| 1049 | + pts.push({ |
| 1050 | + x: cx + r * Math.cos(θ), |
| 1051 | + y: cy + r * Math.sin(θ), |
| 1052 | + vx: 0, vy: 0, pinned: false |
| 1053 | + }); |
| 1054 | + } |
| 1055 | + return { |
| 1056 | + chain: pts, |
| 1057 | + color: colorArr, |
| 1058 | + baseDotRadius: r * 0.12, |
| 1059 | + isClosed: true, |
| 1060 | + frozen: false |
| 1061 | + }; |
| 1062 | + } |
| 1063 | + |
| 1064 | + /* Apply button */ |
| 1065 | + $("applyBlobGrid").onclick = () => { |
| 1066 | + /* 1 ─ grid sizing */ |
| 1067 | + const cnt = Math.min(10, |
| 1068 | + Math.max(1, parseInt($("blobGridCount").value, 10) || 1)); |
| 1069 | + const maxR = Math.min(canvas.width, canvas.height) / (cnt * 2); |
| 1070 | + |
| 1071 | + const baseR = $("blobAutoSize").checked |
| 1072 | + ? maxR |
| 1073 | + : Math.min(parseFloat($("blobBaseRadius").value) || 4, maxR); |
| 1074 | + |
| 1075 | + const spacing = 2 * maxR; // centres stay fixed |
| 1076 | + |
| 1077 | + const chosenColor = hexToRGBA($("blobColor").value); |
| 1078 | + /* 2 ─ add blobs */ |
| 1079 | + for (let r = 0; r < cnt; r++) { |
| 1080 | + for (let c = 0; c < cnt; c++) { |
| 1081 | + const cx = maxR + c * spacing; |
| 1082 | + const cy = maxR + r * spacing; |
| 1083 | + const blob = makeCircleBlob(cx, cy, baseR, chosenColor); |
| 1084 | + chains.push(blob); |
| 1085 | + history.push({ type: "chain" }); |
| 1086 | + } |
| 1087 | + } |
| 1088 | + |
| 1089 | + /* 3 ─ snap UI states exactly like circle-grid did */ |
| 1090 | + { |
| 1091 | + if (interRepelMult !== 3) { |
| 1092 | + interRepelMult = 3; |
| 1093 | + toggleActiveIcon($("iconDiffusion"), true); |
| 1094 | + } |
| 1095 | + setDrawMode("blob"); |
| 1096 | + $("dampingSlider").value = 40; |
| 1097 | + $("dampingValue").textContent = "40%"; |
| 1098 | + setDotScale(12); |
| 1099 | + damping = 0.40; |
| 1100 | + if (!solidFillMode) { |
| 1101 | + solidFillMode = true; |
| 1102 | + $("iconFill").title = "Solid Fill On"; |
| 1103 | + toggleActiveIcon($("iconFill"), true); |
| 1104 | + } |
| 1105 | + slicingMode = dragMode = pinMode = freezeMode = deleteMode = false; |
| 1106 | + $("editToolsSection").classList.remove("edit-on"); |
| 1107 | + ["iconSlice","iconDrag","iconPin","iconFreeze","iconDelete"] |
| 1108 | + .forEach(id=>setIconActive($(id),false,"","")); |
| 1109 | + updateCanvasCursor(); |
| 1110 | + } |
| 1111 | + |
| 1112 | + toast(`Blob grid (${cnt}×${cnt}) added`, 1600); |
| 1113 | + $("blobGridBackdrop").style.display = "none"; |
| 1114 | + }; |
| 1115 | + $("cancelBlobGrid").onclick = () => |
| 1116 | + $("blobGridBackdrop").style.display = "none"; |
| 1117 | + |
802 | 1118 | /* -----------------------------------
|
803 | 1119 | WEBGL CONTEXT
|
804 | 1120 | ----------------------------------- */
|
|
0 commit comments