WEBSITE - WIP - icon shapes - manual position coordinates + angle center mode

This commit is contained in:
Alex Sim committed 2026-10-11 18:17:27 +02:00
1 parent b0a8363ac2
commit a172b9e8f4
3 files changed
+59 -13

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ description: >-
url: "https://alesimula.github.io"
baseurl: ""
version: "1.0.25" #Website version - increase to invalidate cached resources
version: "1.0.26" #Website version - increase to invalidate cached resources
github_repo: "alesimula/Murine-launcher"
icon_url: "https://raw.githubusercontent.com/alesimula/Murine-launcher/refs/heads/main/image/app_icon/icon_browser_fullsize.svg"
icon_freeform_url: "https://raw.githubusercontent.com/alesimula/Murine-launcher/refs/heads/main/image/app_icon/icon_freeform.svg"
+37 -10
View File
@@ -282,9 +282,9 @@
});
return { x: x / (3 * a), y: y / (3 * a) };
}
// The point a shape rotates and centres around, as picked in the Move pill: the middle of its box, or its
// centre of mass. Only the box one moves as the shape turns.
let pivotMode = 'box';
// The point a shape rotates and centres around, as picked in the Move pill: the middle of its box, one of its
// corners, or its centre of mass. Only the centre of mass stays put as the shape turns.
let pivotMode = 'box', pivotCorner = 0; // 'box', 'mass' or 'corner'; the corner as in boxCorners()
// Measured from the outline at 6 decimals: at 2, turning a shape and turning it back would pivot around
// two slightly different points and not land exactly where it started.
const pivot = (sh) => {
@@ -292,7 +292,7 @@
try {
if (pivotMode === 'mass') return centroid(sh);
const b = measure(shapePath(sh));
return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
return pivotMode === 'corner' ? boxCorners(b)[pivotCorner] : { x: b.x + b.width / 2, y: b.y + b.height / 2 };
} finally { digits = 2; }
};
// Turns a shape by deg (clockwise) around its pivot; a stretch turns with it. rot is the running total the
@@ -337,6 +337,7 @@
const STORE = 'murine-shape-editor';
let shapes, active = 0, mode = 'edit', sel = -1, drag = null, sliding = false;
let turnTyping = false; // the rotation pill shows a number field instead of the slider
let pillView = 'pos'; // which half of the Move pill is open: the centre point's position, or 'rot'ation
let pickedCorner = -1; // Move mode: the selected resize corner (0 top left, clockwise), -1 for none
let pts = null, fixed = false; // shortcuts into shapes[active]: its points, or whether its outline is fixed
let synced = '';
@@ -588,6 +589,10 @@
$('se-unstretch').hidden = !shapes[active].st;
for (const b of $('se-mirror').querySelectorAll('[data-mirror]')) b.setAttribute('aria-pressed', b.dataset.mirror === shapes[active].mirror);
for (const b of $('se-mirror').querySelectorAll('[data-pivot]')) b.setAttribute('aria-pressed', b.dataset.pivot === pivotMode);
const [cx, cy] = [[6, 6], [18, 6], [18, 18], [6, 18]][pivotCorner];
$('se-corner-dot').setAttribute('cx', cx); $('se-corner-dot').setAttribute('cy', cy);
for (const b of $('se-corner-pick').children) b.setAttribute('aria-pressed', +b.dataset.corner === pivotCorner);
if (!moving) $('se-corner-pick').hidden = true;
if (moving) { const c = pivot(shapes[active]); $('se-pivot-mark').setAttribute('transform', `translate(${c.x} ${c.y})`); }
// The stretch marks: in Move mode on the pivot cross, past the end of its arms; in Edit mode from the
// centre of mass, around a dot.
@@ -614,8 +619,12 @@
if (moving && !at) {
const rot = shapes[active].rot || 0;
$('se-turn').value = rot; $('se-turn-val').value = Math.round(rot) % 360 + '°'; $('se-turn-num').value = fmt(r2(rot) % 360);
$('se-turn').hidden = $('se-turn-val').hidden = turnTyping; $('se-turn-type').hidden = !turnTyping;
$('se-turn-mode').setAttribute('aria-pressed', turnTyping);
const rotOpen = pillView === 'rot';
$('se-turn').hidden = $('se-turn-val').hidden = !rotOpen || turnTyping; $('se-turn-type').hidden = !rotOpen || !turnTyping;
$('se-turn-mode').setAttribute('aria-pressed', rotOpen);
$('se-pos-mode').setAttribute('aria-pressed', !rotOpen);
$('se-pos-fields').hidden = rotOpen;
if (!rotOpen) { const c = pivot(shapes[active]); $('se-px').value = fmt(c.x); $('se-py').value = fmt(c.y); }
}
if (at) {
$('se-x').value = fmt(at.x); $('se-y').value = fmt(at.y);
@@ -914,9 +923,15 @@
sync(); render();
};
$('se-mirror').addEventListener('click', (e) => {
const p = e.target.closest('[data-pivot]');
if (p) { pivotMode = p.dataset.pivot; render(); }
const p = e.target.closest('[data-pivot]'), c = e.target.closest('[data-corner]');
// The corner mode opens a small picker of the four corners above it (and again when tapped again).
if (c) { pivotCorner = +c.dataset.corner; $('se-corner-pick').hidden = true; render(); }
else if (p) {
$('se-corner-pick').hidden = p.dataset.pivot !== 'corner' || (pivotMode === 'corner' && !$('se-corner-pick').hidden);
pivotMode = p.dataset.pivot; render();
}
});
document.addEventListener('pointerdown', (e) => { if (!e.target.closest('.se-corner-wrap')) $('se-corner-pick').hidden = true; });
$('se-mirror').addEventListener('click', (e) => {
const b = e.target.closest('[data-mirror]');
if (!b || b.dataset.mirror === shapes[active].mirror) return;
@@ -969,7 +984,7 @@
let ghostTimer = 0;
const ghost = (c) => {
clearTimeout(ghostTimer);
if (pivotMode !== 'box') return;
if (pivotMode === 'mass') return;
$('se-pivot-ghost').setAttribute('transform', `translate(${c.x} ${c.y})`);
$('se-pivot-ghost').classList.remove('off');
};
@@ -981,11 +996,23 @@
});
['change', 'blur'].forEach((t) => $('se-turn').addEventListener(t, () => { turnFrom = null; ghostOff(); }));
// The icon switches between the slider and typing the angle; any typed angle is wrapped into 0-359.
// The rotation icon opens the rotation half of the pill; tapped again, it switches slider and typing.
$('se-turn-mode').onclick = () => {
turnTyping = !turnTyping;
if (pillView !== 'rot') pillView = 'rot'; else turnTyping = !turnTyping;
render();
if (turnTyping) $('se-turn-num').select();
};
$('se-pos-mode').onclick = () => { pillView = 'pos'; render(); };
// Typing a position moves the shape so its centre point (as picked in the bottom pill) lands there.
['x', 'y'].forEach((k) => $('se-p' + k).addEventListener('change', (e) => {
const v = clamp(+e.target.value, view.lo, view.hi), sh = shapes[active];
if (e.target.value === '' || !Number.isFinite(v)) return render(); // puts the current value back
const d = v - pivot(sh)[k];
if (Math.abs(d) < 1e-9) return render();
checkpoint();
shapes[active] = transform(sh, 1, 0, 0, k === 'x' ? d : 0, k === 'y' ? d : 0);
sync(); render();
}));
$('se-turn-num').addEventListener('change', (e) => {
const v = +e.target.value, sh = shapes[active];
if (e.target.value === '' || !Number.isFinite(v)) return render(); // puts the current value back
+21 -2
View File
@@ -92,6 +92,9 @@ noindex: true # and out of search engines
background: var(--md-surface-container-low); box-shadow: var(--md-shadow); font-size: .82rem; }
.se-pill:has(#se-remove[hidden]) { padding-right: 8px; }
#se-rot-pill { padding: 3px 10px 3px 3px; }
#se-rot-pill:has(#se-turn-mode:last-of-type[aria-pressed="false"]) { padding-right: 3px; }
.se-pill-part { display: flex; align-items: center; gap: 8px; }
.se-pill svg { width: 20px; height: 20px; }
.se-pill[hidden], .se-pill [hidden] { display: none; }
.se-pill label { display: flex; align-items: center; gap: 4px; color: var(--md-on-surface-variant); }
.se-pill input { width: 4.2em; height: 28px; padding: 0 6px; font: inherit; color: var(--md-on-surface);
@@ -148,6 +151,11 @@ noindex: true # and out of search engines
.se-mirror button { width: 34px; height: 34px; border: 0; background: transparent; }
.se-mirror button[aria-pressed="true"] { background: var(--md-primary-container); color: var(--md-on-primary-container); }
.se-mirror svg { width: 20px; height: 20px; }
.se-corner-wrap { position: relative; display: flex; }
.se-corner-pick { position: absolute; left: 50%; bottom: calc(100% + 10px); transform: translateX(-50%); display: grid;
grid-template-columns: 34px 34px; gap: 2px; padding: 3px; border-radius: 18px; border: 1px solid var(--md-outline-variant);
background: var(--md-surface-container-low); box-shadow: var(--md-shadow); }
.se-corner-pick[hidden] { display: none; }
.se-pivot { display: flex; gap: 2px; padding-left: 5px; margin-left: 3px; border-left: 1px solid var(--md-outline-variant); }
.se-pivot-ghost { pointer-events: none; fill: none; stroke: var(--md-primary); stroke-width: .55; stroke-linecap: round; opacity: .8; transition: opacity .6s; }
.se-pivot-ghost.off { opacity: 0; }
@@ -265,11 +273,22 @@ noindex: true # and out of search engines
<button type="button" data-mirror="both" title="Mirror both ways" aria-label="Mirror both ways"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2v16M2 10h16" stroke="currentColor" stroke-width="1.2" stroke-dasharray="2 1.6" fill="none"/><path d="M2 2h5l-5 5zM18 2h-5l5 5zM2 18h5l-5-5zM18 18h-5l5-5z"/></svg></button>
<span class="se-pivot" role="group" aria-label="Centre point, for rotating and centring">
<button type="button" data-pivot="box" title="Rotate and centre around the middle of the box" aria-label="Middle of the box"><svg viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle cx="12" cy="12" r="2.4" fill="currentColor"/></svg></button>
<span class="se-corner-wrap">
<button type="button" data-pivot="corner" title="Rotate and centre around a corner of the box" aria-label="A corner of the box"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle id="se-corner-dot" cx="6" cy="6" r="2.6" fill="currentColor"/></svg></button>
<span id="se-corner-pick" class="se-corner-pick" role="group" aria-label="Which corner" hidden>
<button type="button" data-corner="0" aria-label="Top left corner"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle cx="6" cy="6" r="2.6" fill="currentColor"/></svg></button><button type="button" data-corner="1" aria-label="Top right corner"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle cx="18" cy="6" r="2.6" fill="currentColor"/></svg></button><button type="button" data-corner="3" aria-label="Bottom left corner"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle cx="6" cy="18" r="2.6" fill="currentColor"/></svg></button><button type="button" data-corner="2" aria-label="Bottom right corner"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="3 2"/><circle cx="18" cy="18" r="2.6" fill="currentColor"/></svg></button>
</span>
</span>
<button type="button" data-pivot="mass" title="Rotate and centre around the centre of mass" aria-label="Centre of mass"><svg viewBox="0 0 24 24"><path d="M12 3.5 21 19.5H3Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="12" cy="14.2" r="1.6" fill="currentColor"/><path d="M12 10.6v1.6M12 16.2v1.6M8.4 14.2h1.6M14 14.2h1.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
</span>
</div>
<div id="se-rot-pill" class="se-pill" role="group" aria-label="Rotation" hidden>
<button id="se-turn-mode" type="button" aria-pressed="false" title="Type the angle" aria-label="Type the angle"><span class="material-symbols-outlined" aria-hidden="true">&#xe41a;</span></button>
<div id="se-rot-pill" class="se-pill" role="group" aria-label="Position and rotation" hidden>
<button id="se-pos-mode" type="button" aria-pressed="true" title="Position of the centre point" aria-label="Position"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4.5"/><path d="M12 2.5v4M12 17.5v4M2.5 12h4M17.5 12h4"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/></svg></button>
<span id="se-pos-fields" class="se-pill-part">
<label>X <input id="se-px" type="number" step="0.5" inputmode="decimal" aria-label="Centre point X"></label>
<label>Y <input id="se-py" type="number" step="0.5" inputmode="decimal" aria-label="Centre point Y"></label>
</span>
<button id="se-turn-mode" type="button" aria-pressed="false" title="Rotation (tap again to type the angle)" aria-label="Rotation"><span class="material-symbols-outlined" aria-hidden="true">&#xe41a;</span></button>
<input id="se-turn" type="range" min="0" max="359" step="1" aria-label="Rotation in degrees, clockwise"><output id="se-turn-val" for="se-turn"></output>
<label id="se-turn-type" hidden><input id="se-turn-num" type="number" step="1" aria-label="Rotation in degrees, clockwise">°</label>
</div>