mirror of
https://github.com/alesimula/Murine-launcher.git
synced 2026-10-28 09:43:42 +00:00
660 lines
55 KiB
HTML
660 lines
55 KiB
HTML
---
|
||
layout: page
|
||
title: Icon shapes
|
||
subtitle: Pick a preset or draw your own shape, then send it to Murine
|
||
permalink: /icon-shapes/
|
||
sitemap: false # keep it out of sitemap.xml
|
||
noindex: true # and out of search engines
|
||
---
|
||
|
||
<style>
|
||
.page-body { max-width: 960px; }
|
||
.se-presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 10px; margin: 1rem 0 2rem; }
|
||
.se-preset { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 6px 10px;
|
||
border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-md);
|
||
background: var(--md-surface-container-low); color: var(--md-on-surface); cursor: pointer;
|
||
font: inherit; font-size: .78rem; line-height: 1.2; }
|
||
.se-preset:hover { background: var(--md-surface-container-high); }
|
||
.se-preset > svg { width: 48px; height: 48px; fill: var(--md-primary); }
|
||
.se-preset[hidden] { display: none; }
|
||
#se-presets { margin-bottom: 12px; }
|
||
.se-presets-more { display: block; margin: 0 auto 2rem; }
|
||
|
||
.se-editor { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 24px; align-items: start; }
|
||
@media (max-width: 760px) { .se-editor { grid-template-columns: 1fr; } }
|
||
#se-canvas { width: 100%; aspect-ratio: 1; display: block; touch-action: manipulation; user-select: none;
|
||
background: var(--md-surface-container-lowest); border: 1px solid var(--md-outline-variant);
|
||
border-radius: var(--md-shape-lg); outline: none; }
|
||
/* Focus ring only when the canvas was reached with the keyboard: clicks and taps focus it too (so Delete
|
||
and the arrow keys work), and Chrome would then show the ring as soon as a key is pressed. */
|
||
#se-canvas:focus-visible:not([data-pointer]) { box-shadow: 0 0 0 2px var(--md-primary); }
|
||
.se-grid-line { stroke: var(--md-outline-variant); stroke-width: .15; }
|
||
.se-grid-mid { stroke: var(--md-outline); stroke-width: .25; }
|
||
.se-bounds { fill: none; stroke: var(--md-outline); stroke-width: .35; stroke-dasharray: 1.5 1; }
|
||
/* Every shape is filled on its own: the others softly and behind, the selected one stronger and on top. */
|
||
.se-stage { --se-fill-active: color-mix(in srgb, var(--md-primary) 50%, transparent);
|
||
--se-fill-other: color-mix(in srgb, var(--md-primary-container) 85%, transparent); }
|
||
[data-theme="dark"] .se-stage { --se-fill-active: color-mix(in srgb, var(--md-primary-container) 85%, transparent);
|
||
--se-fill-other: color-mix(in srgb, var(--md-primary-container) 30%, transparent); }
|
||
.se-line { fill: var(--se-fill-other); stroke: var(--md-outline); stroke-width: .35; stroke-dasharray: 1.2 1; }
|
||
.se-line.active { fill: var(--se-fill-active); }
|
||
.se-line.copy { stroke-dasharray: .4 .8; }
|
||
.se-line.active { stroke: var(--md-primary); stroke-width: .5; stroke-dasharray: none; }
|
||
.se-line.active.copy { stroke-width: .35; stroke-dasharray: 1.2 1; }
|
||
#se-back-fill { filter: url(#se-cap-light); }
|
||
[data-theme="dark"] #se-back-fill { filter: url(#se-cap-dark); }
|
||
#se-back-fill .se-line { stroke: none; }
|
||
#se-back-stroke .se-line { fill: none; }
|
||
.se-move-box { fill: transparent; stroke: var(--md-primary); stroke-width: .3; stroke-dasharray: 1 1; cursor: move; }
|
||
.se-resize { fill: var(--md-surface-container-lowest); stroke: var(--md-primary); stroke-width: .5; }
|
||
.se-resize.selected { fill: var(--md-primary); }
|
||
.se-move-box, .se-resize { touch-action: none; }
|
||
.se-outline { fill: none; stroke: var(--md-on-surface-variant); stroke-width: .25; stroke-dasharray: 1 1; }
|
||
.se-handle { cursor: grab; outline: none; }
|
||
.se-handle circle:not(.se-hit) { fill: var(--md-surface-container-lowest); stroke: var(--md-primary); stroke-width: .7; }
|
||
.se-handle.selected circle:not(.se-hit), .se-handle:focus-visible circle:not(.se-hit) { fill: var(--md-primary); }
|
||
.se-hit { fill: transparent; }
|
||
.se-add { cursor: copy; opacity: .55; }
|
||
.se-add:hover { opacity: 1; }
|
||
.se-handle, .se-add { touch-action: none; }
|
||
.se-add circle:not(.se-hit) { fill: var(--md-surface-container-lowest); stroke: var(--md-outline); stroke-width: .35; }
|
||
.se-add path { stroke: var(--md-on-surface-variant); stroke-width: .45; }
|
||
|
||
.se-panel fieldset { border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-md);
|
||
padding: 10px 14px 12px; margin: 0 0 12px; }
|
||
.se-panel legend { padding: 0 6px; font-weight: 600; font-size: .85rem; }
|
||
/* Corner size and smoothing as sliders, or (.se-typed, with the legend's toggle) as number fields */
|
||
#se-corner-set legend { display: flex; align-items: center; gap: 6px; }
|
||
.se-corner-type { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 30px; padding: 0;
|
||
border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-full); background: transparent; color: var(--md-primary);
|
||
font: inherit; font-size: .78rem; font-weight: 600; letter-spacing: -.02em; cursor: pointer; }
|
||
.se-corner-type:hover { background: var(--md-surface-container); }
|
||
.se-corner-type svg { width: 22px; height: 22px; }
|
||
/* fingers: an invisible margin around it that still counts as the button */
|
||
@media (pointer: coarse) { .se-corner-type::after { content: ''; position: absolute; inset: -8px -4px; } }
|
||
.se-ct-slide, .se-typed .se-ct-num, .se-num { display: none; }
|
||
.se-typed .se-ct-slide { display: block; }
|
||
#se-corner-set.se-typed .se-num { display: inline-flex; align-items: center; gap: 4px; }
|
||
#se-corner-set.se-typed :is(#se-size-v, #se-smooth-v, #se-size-track, #se-smooth, .se-scale) { display: none; }
|
||
.se-panel .se-num, .se-panel .se-num input { user-select: text; -webkit-user-select: text; }
|
||
.se-unit { min-width: 1.2em; } /* the same for both fields, so they line up */
|
||
#se-size-v.se-over { color: var(--md-tertiary); cursor: help; }
|
||
.se-panel label { display: flex; align-items: center; justify-content: space-between; gap: 8px; user-select: none; -webkit-user-select: none;
|
||
font-size: .88rem; margin: 4px 0; }
|
||
.se-panel input[type=range] { width: 100%; accent-color: var(--md-primary); }
|
||
/* Tinted end of the size slider: past it, the selected corner shrinks its neighbours' rounding.
|
||
8px is about half the native thumb, so the tint starts under the thumb's centre. */
|
||
/* Native range inputs ignore their own background, so the tint sits on a wrapper behind the input. */
|
||
.se-size-track { display: flex; }
|
||
.se-size-track.has-zone { background: linear-gradient(to right, transparent calc(8px + (100% - 16px) * var(--squeeze)),
|
||
color-mix(in srgb, var(--md-tertiary) 55%, transparent) 0) 0 100% / 100% 4px no-repeat; }
|
||
.se-size-track input { margin-bottom: 6px; }
|
||
.se-panel .se-size-warn { color: var(--md-on-surface-variant); font-size: .78rem; line-height: 1.4; min-height: 2.8em; margin: 0 0 4px; }
|
||
.se-panel .se-size-warn.active { color: var(--md-tertiary); }
|
||
.se-handle.squeezed circle:not(.se-hit) { stroke: var(--md-tertiary);
|
||
fill: color-mix(in srgb, var(--md-tertiary) 35%, var(--md-surface-container-lowest)); }
|
||
/* filled means selected (as above), red means a warning: both, a solid red dot */
|
||
.se-handle.squeezed.selected circle:not(.se-hit), .se-handle.squeezed:focus-visible circle:not(.se-hit) { fill: var(--md-tertiary); }
|
||
.se-panel input[type=number], .se-panel select { width: 5.5em; font: inherit; padding: 2px 6px;
|
||
color: var(--md-on-surface); background: var(--md-surface-container); border: 1px solid var(--md-outline-variant);
|
||
border-radius: var(--md-shape-xs); }
|
||
.se-panel input[type=checkbox] { accent-color: var(--md-primary); }
|
||
.se-scale { display: flex; justify-content: space-between; font-size: .72rem; color: var(--md-on-surface-variant); margin-top: -4px; }
|
||
.se-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||
.se-btn { font: inherit; font-size: .82rem; padding: 6px 12px; border-radius: var(--md-shape-full); cursor: pointer;
|
||
border: 1px solid var(--md-outline); background: transparent; color: var(--md-primary); }
|
||
.se-btn:hover:not(:disabled) { background: var(--md-surface-container); }
|
||
.se-btn:disabled { opacity: .4; cursor: default; }
|
||
.se-toolbar { margin: 0 0 12px; }
|
||
/* Top-left pill: X / Y of the selected point (and its delete button) or of the selected Move-mode corner.
|
||
Same height, border and shadow as the mirror pill. */
|
||
.se-pill { position: absolute; left: 10px; top: 10px; display: flex; align-items: center; gap: 8px; padding: 3px 3px 3px 12px;
|
||
border-radius: var(--md-shape-full); border: 1px solid var(--md-outline-variant);
|
||
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);
|
||
background: var(--md-surface-container); border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-xs); }
|
||
.se-pill button { width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; padding: 0;
|
||
border: 0; border-radius: var(--md-shape-full); background: transparent; color: var(--md-primary); cursor: pointer; }
|
||
.se-pill button:hover:not(:disabled):not([aria-pressed="true"]) { background: var(--md-surface-container-high); }
|
||
.se-pill button:disabled { opacity: .35; cursor: default; }
|
||
.se-pill button[aria-pressed="true"] { background: var(--md-primary-container); color: var(--md-on-primary-container); }
|
||
.se-pill .material-symbols-outlined { font-size: 20px; }
|
||
.se-pill input[type=range] { width: 120px; height: auto; padding: 0; border: 0; background: none; accent-color: var(--md-primary); }
|
||
#se-turn-val { min-width: 4ch; text-align: right; color: var(--md-on-surface-variant); font-variant-numeric: tabular-nums; }
|
||
@media (pointer: fine) { .se-pill input { width: calc(6ch + 22px); } } /* room for "-12.50" plus the spin arrows */
|
||
.se-hint { font-size: .8rem; color: var(--md-on-surface-variant); line-height: 1.5; margin: 8px 0 0; }
|
||
|
||
.se-result { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 20px; align-items: center; margin-top: 1rem; }
|
||
@media (max-width: 640px) { .se-result { grid-template-columns: 1fr; } }
|
||
.se-previews { display: flex; align-items: end; gap: 14px; }
|
||
.se-preview { fill: url(#se-icon-fill); }
|
||
#se-out { width: 100%; min-height: 6.5em; font-family: ui-monospace, monospace; font-size: .78rem; resize: vertical;
|
||
color: var(--md-on-surface); background: var(--md-surface-container); border: 1px solid var(--md-outline-variant);
|
||
border-radius: var(--md-shape-sm); padding: 8px; }
|
||
.se-warn { color: var(--md-error); font-size: .88rem; margin: 6px 0 0; }
|
||
.se-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
|
||
.se-actions .btn-filled, .se-actions .btn-outlined { padding: 10px 22px; }
|
||
.se-note { margin: 0 0 12px; padding: 10px 14px; border-radius: var(--md-shape-md); background: var(--md-surface-container); }
|
||
.se-panel fieldset:disabled { opacity: .45; }
|
||
.se-disabled { opacity: .4; pointer-events: none; }
|
||
#se-use, #se-use:hover { color: var(--md-on-primary); text-decoration: none; }
|
||
/* The share dialog reuses the contact dialog's classes; undo the prose styles of .page-body around it. */
|
||
#se-share { line-height: 1.5; }
|
||
#se-share h2 { margin: 0; }
|
||
#se-share .se-card-title { display: block; font-size: 1rem; font-weight: 500; margin-bottom: 2px; }
|
||
#se-share .se-card-sub { display: block; font-size: .8125rem; color: var(--md-on-surface-variant); }
|
||
#se-share .contact-modal-desc { margin: 0 0 24px; }
|
||
.se-share-card { width: 100%; border: 0; font: inherit; text-align: left; cursor: pointer; }
|
||
.se-share-card[hidden] { display: none; }
|
||
.se-share-card:disabled { opacity: .45; cursor: default; box-shadow: none; }
|
||
.se-dev { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
||
.se-dev[hidden] { display: none; }
|
||
.se-dev .se-share-card { flex-direction: column; justify-content: center; gap: 2px; padding: 10px 6px; text-align: center; }
|
||
.se-dev .material-symbols-outlined { color: var(--md-primary); }
|
||
/* Canvas overlay (Move mode): fit this shape top right, mirroring bottom left. */
|
||
.se-left { min-width: 0; }
|
||
.se-stage { position: relative; }
|
||
.se-docs { margin-top: 40px; font-size: .9rem; }
|
||
.se-docs summary { cursor: pointer; font-weight: 500; color: var(--md-primary); }
|
||
.se-docs-links a { overflow-wrap: anywhere; }
|
||
/* "Hide tools": everything floating over the canvas goes (the canvas is the stage's only svg child) */
|
||
.se-stage.se-bare > :not(svg) { display: none !important; }
|
||
.se-title-row { display: flex; align-items: center; gap: 16px; margin: 40px 0 16px; }
|
||
.page-body .se-title-row h2 { margin: 0; }
|
||
.se-shake { animation: se-shake .35s; }
|
||
@keyframes se-shake { 20%, 60% { transform: translateX(-6px); } 40%, 80% { transform: translateX(6px); } }
|
||
@media (prefers-reduced-motion: reduce) { .se-shake { animation: none; } }
|
||
.se-stage-btn, .se-mirror button { display: inline-flex; align-items: center; justify-content: center; padding: 0;
|
||
border-radius: var(--md-shape-full); color: var(--md-primary); cursor: pointer; }
|
||
.se-stage-tools { position: absolute; top: 10px; right: 10px; display: flex; flex-direction: column; gap: 6px; }
|
||
.se-stage-btn { width: 40px; height: 40px;
|
||
border: 1px solid var(--md-outline-variant); background: var(--md-surface-container-low); box-shadow: var(--md-shadow); }
|
||
.se-mirror { position: absolute; left: 10px; bottom: 10px; display: flex; gap: 2px; padding: 3px;
|
||
border-radius: var(--md-shape-full); border: 1px solid var(--md-outline-variant);
|
||
background: var(--md-surface-container-low); box-shadow: var(--md-shadow); }
|
||
.se-mirror button { width: 34px; height: 34px; border: 0; background: transparent; }
|
||
.se-mirror button:disabled { opacity: .35; cursor: default; pointer-events: none; }
|
||
.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; }
|
||
.se-pivot-mark { pointer-events: none; fill: none; stroke: var(--md-primary); stroke-width: .6; stroke-linecap: round; stroke-linejoin: round; }
|
||
.se-stage-btn:hover, .se-mirror button:hover:not([aria-pressed="true"]) { background: var(--md-surface-container-high); }
|
||
.se-stage-tools[hidden], .se-mirror[hidden], .se-stage-btn[hidden] { display: none; }
|
||
/* Bottom right: the "how it works" button and its bubble, which opens above it */
|
||
.se-help-btn { position: absolute; right: 10px; bottom: 10px; width: 36px; height: 36px; }
|
||
.se-help-btn[aria-expanded="true"] { background: var(--md-primary-container); color: var(--md-on-primary-container); }
|
||
.se-help { position: absolute; right: 10px; bottom: 54px; width: min(340px, calc(100% - 20px)); max-height: calc(100% - 64px);
|
||
overflow-y: auto; padding: 10px 14px; border-radius: var(--md-shape-md); border: 1px solid var(--md-outline-variant);
|
||
background: var(--md-surface-container-low); box-shadow: var(--md-shadow); font-size: .8rem; line-height: 1.5;
|
||
color: var(--md-on-surface-variant); z-index: 2; }
|
||
.se-help[hidden] { display: none; }
|
||
.se-help p { margin: 0; }
|
||
.se-help p + p { margin-top: 6px; }
|
||
.se-help strong { color: var(--md-on-surface); font-weight: 500; }
|
||
.se-touch { display: none; }
|
||
@media (hover: none) { .se-kb { display: none; } .se-touch { display: inline; } }
|
||
@media (max-width: 365px) { /* no room beside the mirror pill (267px wide), nor under the right-hand column: above the pill */
|
||
.se-help-btn { bottom: 56px; right: auto; left: 10px; }
|
||
.se-help { bottom: 100px; right: auto; left: 10px; max-height: calc(100% - 110px); }
|
||
}
|
||
.se-stage-btn svg { width: 22px; height: 22px; }
|
||
/* Shape cards under the canvas. */
|
||
.se-cards { display: flex; gap: 10px; overflow-x: auto; padding: 12px 2px 6px; }
|
||
.se-card { position: relative; flex: 0 0 104px; display: flex; flex-direction: column; align-items: center;
|
||
border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-md); background: var(--md-surface-container-low); }
|
||
.se-card.active { border-color: var(--md-primary); box-shadow: 0 0 0 1px var(--md-primary); }
|
||
.se-card-pick { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; padding: 12px 6px 8px;
|
||
border: 0; background: transparent; color: var(--md-on-surface); font: inherit; font-size: .75rem; cursor: pointer;
|
||
border-radius: var(--md-shape-md); user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
|
||
.se-card-pick svg { width: 56px; height: 56px; }
|
||
.se-card-pick path { fill: var(--md-primary); }
|
||
.se-card-pick path.copy { fill-opacity: .35; }
|
||
.se-card-box { fill: none; stroke: var(--md-outline-variant); stroke-width: 2; stroke-dasharray: 6 4; }
|
||
.se-card-del { position: absolute; top: 2px; right: 2px; width: 28px; height: 28px; display: flex; align-items: center;
|
||
justify-content: center; padding: 0; border: 0; border-radius: 50%; background: transparent;
|
||
color: var(--md-on-surface-variant); cursor: pointer; }
|
||
.se-card-del:hover { background: var(--md-surface-container-highest); color: var(--md-error); }
|
||
.se-card-del .material-symbols-outlined, .se-card-modes .material-symbols-outlined { font-size: 18px; }
|
||
.se-card-modes { display: flex; gap: 4px; padding: 0 6px 8px; }
|
||
.se-card-modes button { width: 38px; height: 30px; display: flex; align-items: center; justify-content: center; padding: 0;
|
||
border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-full); background: transparent;
|
||
color: var(--md-on-surface-variant); cursor: pointer; }
|
||
.se-card-modes button[aria-pressed="true"] { background: var(--md-primary-container); color: var(--md-on-primary-container); border-color: transparent; }
|
||
.se-card-modes button:disabled { opacity: .35; cursor: default; }
|
||
.se-card-add { justify-content: center; min-height: 104px; border-style: dashed; color: var(--md-primary); cursor: pointer; font: inherit; }
|
||
.se-card-add .material-symbols-outlined { font-size: 32px; }
|
||
.se-card-add:hover { background: var(--md-surface-container-high); }
|
||
.se-note-out { color: var(--md-on-surface-variant); font-size: .85rem; margin: 6px 0 0; }
|
||
/* The "add a shape" dialog reuses the contact dialog's classes too. */
|
||
#se-add { line-height: 1.5; }
|
||
#se-add h2 { margin: 0; }
|
||
#se-add .contact-modal { max-width: 640px; }
|
||
#se-add .contact-modal-desc { margin: 0 0 16px; }
|
||
/* The dialog never grows past the screen: only the grid scrolls, so the buttons above it and the bar
|
||
under it stay in view (the whole dialog scrolls only if even a two-row grid doesn't fit). */
|
||
#se-add .contact-modal { max-height: 100%; display: flex; flex-direction: column; overflow-y: auto; }
|
||
#se-add .contact-modal > * { flex: none; }
|
||
#se-add .contact-modal > .se-presets { flex: 1 1 auto; min-height: 220px; margin: 0; overflow-y: auto; align-content: start; } /* two rows of tiles; extra height stays empty instead of stretching them */
|
||
@media (max-width: 480px) { #se-add { padding: 12px; } }
|
||
/* On a very short screen the whole dialog scrolls: the bar then sticks to its bottom edge. */
|
||
#se-add .se-pick-bar { position: sticky; bottom: 0; padding: 8px 0; margin-top: 4px; background: var(--md-surface-container-low); }
|
||
.se-import { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin: 0 0 16px; }
|
||
.se-import p { margin: 0; }
|
||
#se-import-choice[hidden], #se-import-msg[hidden], #se-import-unload[hidden], .se-pick-bar[hidden] { display: none; }
|
||
.se-preset.picked { border-color: var(--md-primary); background: var(--md-primary-container); box-shadow: inset 0 0 0 1px var(--md-primary); }
|
||
.se-preset:disabled { opacity: .45; cursor: default; }
|
||
.se-preset { -webkit-touch-callout: none; user-select: none; } /* long-press picks, it doesn't open the callout or select text */
|
||
.se-preset .se-tile-ghost { opacity: .18; }
|
||
.se-seg { display: inline-flex; margin: 8px 0; border: 1px solid var(--md-outline-variant); border-radius: var(--md-shape-full); overflow: hidden; }
|
||
.se-seg[hidden] { display: none; }
|
||
.se-seg button { font: inherit; font-size: .82rem; padding: 6px 14px; border: 0; background: transparent; color: var(--md-on-surface); cursor: pointer; }
|
||
.se-seg button + button { border-left: 1px solid var(--md-outline-variant); }
|
||
.se-seg button[aria-pressed="true"] { background: var(--md-primary-container); color: var(--md-on-primary-container); }
|
||
.se-pick-bar { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||
#se-pick-count { margin-right: auto; font-size: .85rem; color: var(--md-on-surface-variant); }
|
||
/* The buttons stay on one row together: if they don't fit next to the count, they move under it as a group. */
|
||
.se-pick-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-left: auto; }
|
||
.se-btn-primary { background: var(--md-primary); color: var(--md-on-primary); border-color: var(--md-primary); }
|
||
.se-btn-primary:hover:not(:disabled) { background: var(--md-primary); filter: brightness(1.08); }
|
||
/* Saved shapes: the Presets / Custom switch is the section's title; custom tiles get rename and delete corners. */
|
||
.page-body h2.se-lib-title { margin: 40px 0 0; }
|
||
.se-seg-title { margin: 0; }
|
||
.se-seg-title button { font: inherit; padding: 4px 18px; } /* the size of the other section titles */
|
||
.se-seg-title button[aria-pressed="false"] { color: var(--md-on-surface-variant); }
|
||
.se-presets[hidden], .se-presets-more[hidden], .se-saved[hidden] { display: none; }
|
||
#se-add-lib { margin: 0; }
|
||
.se-saved { position: relative; display: flex; }
|
||
.se-saved > .se-preset { flex: 1; min-width: 0; padding-top: 22px; }
|
||
.se-saved > .se-preset span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.se-tile-btn { position: absolute; top: 2px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
|
||
padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--md-on-surface-variant); cursor: pointer; }
|
||
.se-tile-btn .material-symbols-outlined { font-size: 18px; }
|
||
.se-tile-edit { left: 2px; }
|
||
.se-tile-del { right: 2px; }
|
||
.se-tile-btn:hover { background: var(--md-surface-container-highest); color: var(--md-primary); }
|
||
.se-tile-del:hover { color: var(--md-error); }
|
||
.se-card-save { right: auto; left: 2px; }
|
||
.se-card-save:hover { color: var(--md-primary); }
|
||
.se-presets > .se-hint { grid-column: 1 / -1; margin: 0; }
|
||
.se-custom-msg { display: flex; align-items: center; gap: 8px; margin: -4px 0 16px; }
|
||
.se-custom-msg[hidden] { display: none; }
|
||
.se-name-modal { max-width: 400px; }
|
||
.se-name-input { display: block; width: 100%; box-sizing: border-box; margin: 4px 0 4px; padding: 10px 12px; font: inherit;
|
||
color: var(--md-on-surface); background: var(--md-surface-container-lowest); border: 1px solid var(--md-outline);
|
||
border-radius: var(--md-shape-sm, 8px); }
|
||
.se-name-input:focus { outline: 2px solid var(--md-primary); outline-offset: -1px; }
|
||
#se-name .se-pick-actions { margin-top: 16px; }
|
||
#se-measure { position: absolute; width: 0; height: 0; overflow: hidden; }
|
||
</style>
|
||
|
||
<h2 class="se-lib-title"><span id="se-lib" class="se-seg se-seg-title" role="group" aria-label="Shapes to start from">
|
||
<button type="button" data-lib="presets" aria-pressed="true">Presets</button><button type="button" data-lib="custom" aria-pressed="false" title="Shapes you saved, kept in this browser">Custom</button>
|
||
</span></h2>
|
||
<div id="se-presets" class="se-presets"></div>
|
||
<div id="se-custom" class="se-presets" hidden></div>
|
||
<p id="se-custom-msg" class="se-hint se-custom-msg" role="status" hidden><span></span> <button type="button" class="se-btn" id="se-custom-undo" hidden>Undo</button></p>
|
||
<button id="se-presets-more" class="se-btn se-presets-more" type="button" aria-expanded="false" aria-controls="se-presets">More shapes</button>
|
||
|
||
<div class="se-title-row">
|
||
<h2>Editor</h2>
|
||
<button id="se-tools-toggle" class="se-btn" type="button" aria-pressed="false" title="Hide the buttons and pills floating over the canvas">Hide tools</button>
|
||
</div>
|
||
<div class="se-editor">
|
||
<div class="se-left">
|
||
<div class="se-stage">
|
||
<svg id="se-canvas" viewBox="-20 -20 140 140" tabindex="0" aria-label="Shape editor. In Edit mode drag points to move them and the plus marks to add points; in Move mode drag the shape or its corners.">
|
||
<g id="se-grid"></g>
|
||
<rect class="se-bounds" x="0" y="0" width="100" height="100"/>
|
||
<defs>
|
||
<!-- The other shapes' fills add up where they overlap, but only up to a cap, so a pile of them never
|
||
reaches the selected shape's colour: alpha becomes min(alpha, cap). -->
|
||
<filter id="se-cap-light" filterUnits="userSpaceOnUse" x="-1000" y="-1000" width="2200" height="2200"><feComponentTransfer><feFuncA type="table" tableValues="0 .1 .2 .3 .4 .5 .6 .7 .8 .9 .9"/></feComponentTransfer></filter>
|
||
<filter id="se-cap-dark" filterUnits="userSpaceOnUse" x="-1000" y="-1000" width="2200" height="2200"><feComponentTransfer><feFuncA type="table" tableValues="0 .5 .5"/></feComponentTransfer></filter>
|
||
</defs>
|
||
<g id="se-lines"><g id="se-back-fill"></g><g id="se-back-stroke"></g><g id="se-front"></g></g>
|
||
<polygon id="se-outline" class="se-outline"/>
|
||
<g id="se-stretch-mark" class="se-pivot-mark" style="display: none"><circle r=".8" fill="var(--md-primary)" stroke="none"/><path/></g>
|
||
<g id="se-adders"></g>
|
||
<g id="se-handles"></g>
|
||
<g id="se-move" style="display: none">
|
||
<rect id="se-move-box" class="se-move-box"/>
|
||
<!-- corners in the order the script fills them: top left, top right, bottom right, bottom left -->
|
||
<rect class="se-resize" width="4" height="4" style="cursor: nwse-resize"/>
|
||
<rect class="se-resize" width="4" height="4" style="cursor: nesw-resize"/>
|
||
<rect class="se-resize" width="4" height="4" style="cursor: nwse-resize"/>
|
||
<rect class="se-resize" width="4" height="4" style="cursor: nesw-resize"/>
|
||
<!-- sides, in the same order: top, right, bottom, left -->
|
||
<rect class="se-resize" width="6" height="2.2" rx="1.1" style="cursor: ns-resize"/>
|
||
<rect class="se-resize" width="2.2" height="6" rx="1.1" style="cursor: ew-resize"/>
|
||
<rect class="se-resize" width="6" height="2.2" rx="1.1" style="cursor: ns-resize"/>
|
||
<rect class="se-resize" width="2.2" height="6" rx="1.1" style="cursor: ew-resize"/>
|
||
<!-- where a turn around the middle of the box pivots: that middle moves as the shape turns -->
|
||
<g id="se-pivot-ghost" class="se-pivot-ghost off"><circle r="1.8" stroke-dasharray=".7 .7"/><path d="M0 -4V-2.2M0 2.2V4M-4 0H-2.2M2.2 0H4"/></g>
|
||
<g id="se-pivot-mark" class="se-pivot-mark"><circle r="1.8"/><path d="M0 -4V-2.2M0 2.2V4M-4 0H-2.2M2.2 0H4"/><circle r=".5" fill="var(--md-primary)" stroke="none"/><path id="se-stretch-arrows"/></g>
|
||
</g>
|
||
</svg>
|
||
<button id="se-help-btn" class="se-stage-btn se-help-btn" type="button" title="How the editor works" aria-label="How the editor works" aria-expanded="false" aria-controls="se-help"><span class="material-symbols-outlined" aria-hidden="true"></span></button>
|
||
<div id="se-help" class="se-help" role="note" hidden>
|
||
<!-- Keys and Ctrl only where there's likely a keyboard: a primary pointer that can hover (mouse, trackpad). -->
|
||
<p><strong>Edit mode:</strong> drag a point to move it, drag a <strong>+</strong> to add one.
|
||
<span class="se-kb">With a point selected, arrow keys nudge it, Delete removes it, Esc deselects.
|
||
Ctrl+click (or long-press) more points to select several: they move, nudge and delete together.</span>
|
||
<span class="se-touch">Tap a point to type its position or remove it; tap empty space to deselect.
|
||
Long-press more points to select several: they move together.</span></p>
|
||
<p><strong>Move mode:</strong> drag the shape, or a corner to resize it; <span class="se-kb">click a corner to type its
|
||
position. Arrow keys move the shape, Delete removes it.</span><span class="se-touch">tap a corner to type its position.</span>
|
||
The first card, <strong>Global</strong>, moves, turns, resizes or flips the whole shape at once; its reset button
|
||
takes that back.</p>
|
||
<p><strong>Several shapes:</strong> <span class="se-kb">Ctrl+click (or long-press) a shape or its card to move them
|
||
together; Esc stops.</span><span class="se-touch">long-press a shape or its card to move them together, then tap
|
||
cards to add or remove more. Tap outside the group to stop.</span></p>
|
||
<p><strong>Saving:</strong> the save button on a card keeps that shape (on Global, the whole shape) under
|
||
Custom, above the editor and in Add a shape. It stays in this browser.</p>
|
||
</div>
|
||
<div id="se-fit-tools" class="se-stage-tools" hidden>
|
||
<button id="se-fit-shape" class="se-stage-btn" type="button" title="Fit this shape to the box" aria-label="Fit this shape to the box"><span class="material-symbols-outlined" aria-hidden="true"></span></button>
|
||
<button id="se-fit-quarter" class="se-stage-btn" type="button" title="Fit this shape into the top-left quarter" aria-label="Fit this shape into the top-left quarter"><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"/><rect x="5.5" y="5.5" width="6.5" height="6.5" rx="1.5" fill="currentColor"/></svg></button>
|
||
<button id="se-center" class="se-stage-btn" type="button" title="Centre this shape" aria-label="Centre this shape"><span class="material-symbols-outlined" aria-hidden="true"></span></button>
|
||
<button id="se-flip" class="se-stage-btn" type="button" title="Flip left to right, around the pivot" aria-label="Flip left to right"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-dasharray="2.5 2.5"/><path d="M9.5 6 3.5 18h6z" fill="currentColor"/><path d="M14.5 6l6 12h-6z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg></button>
|
||
<button id="se-unstretch" class="se-stage-btn" type="button" title="Remove the stretch (shrinks the long side)" aria-label="Remove the stretch" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18" stroke-dasharray="2.5 2.5"/><path d="M3 12h6M6.5 9.5 9 12l-2.5 2.5M21 12h-6M17.5 9.5 15 12l2.5 2.5"/></svg></button>
|
||
</div>
|
||
<div id="se-mirror" class="se-mirror" role="group" aria-label="Mirror" hidden>
|
||
<button type="button" data-mirror="none" title="No mirroring" aria-label="No mirroring"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M6 4l6 6-6 6z"/></svg></button>
|
||
<button type="button" data-mirror="h" title="Mirror left and right" aria-label="Mirror left and right"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2v16" stroke="currentColor" stroke-width="1.2" stroke-dasharray="2 1.6" fill="none"/><path d="M2 5l5 5-5 5zM18 5l-5 5 5 5z"/></svg></button>
|
||
<button type="button" data-mirror="v" title="Mirror top and bottom" aria-label="Mirror top and bottom"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 10h16" stroke="currentColor" stroke-width="1.2" stroke-dasharray="2 1.6" fill="none"/><path d="M5 2l5 5 5-5zM5 18l5-5 5 5z"/></svg></button>
|
||
<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="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"></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>
|
||
<div id="se-point-pill" class="se-pill" role="group" aria-label="Point" hidden>
|
||
<label>X <input id="se-x" type="number" step="0.5" inputmode="decimal"></label>
|
||
<label>Y <input id="se-y" type="number" step="0.5" inputmode="decimal"></label>
|
||
<button id="se-remove" type="button" title="Remove point" aria-label="Remove point"><span class="material-symbols-outlined" aria-hidden="true"></span></button>
|
||
</div>
|
||
</div>
|
||
<div id="se-cards" class="se-cards" role="list" aria-label="Shapes"></div>
|
||
</div>
|
||
|
||
<div class="se-panel">
|
||
<div class="se-row se-toolbar">
|
||
<button id="se-undo" class="se-btn" type="button" title="Undo (Ctrl+Z)">Undo</button>
|
||
<button id="se-redo" class="se-btn" type="button" title="Redo (Ctrl+Y or Ctrl+Shift+Z)">Redo</button>
|
||
<button id="se-fit" class="se-btn" type="button" title="Scale and centre the shapes until their longest side touches the box (the Global card has its own Fit)">Fit to box</button>
|
||
<button id="se-reset" class="se-btn" type="button">Reset</button>
|
||
</div>
|
||
<p id="se-fixed-note" class="se-hint se-note" hidden>This shape has a fixed outline (ready-made or imported): you can move, resize, stretch and
|
||
mirror it, but not edit its points. Generate replaces it with an editable one.</p>
|
||
<fieldset id="se-corner-set">
|
||
<legend><span id="se-corner-legend">All corners</span>
|
||
<button id="se-corner-type" class="se-corner-type" type="button" aria-pressed="false" title="Type the values instead" aria-label="Type the values instead"><span class="se-ct-num" aria-hidden="true">123</span><svg class="se-ct-slide" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="12" r="4" fill="currentColor"/></svg></button></legend>
|
||
<label for="se-size">Size <span id="se-size-v"></span><span class="se-num"><input id="se-size-n" type="number" min="0" max="500" step="0.1" placeholder="mixed" aria-label="Corner size"><span class="se-unit"></span></span></label>
|
||
<div id="se-size-track" class="se-size-track"><input id="se-size" type="range" min="0" max="50" step="0.1"></div>
|
||
<p id="se-size-warn" class="se-size-warn" style="visibility: hidden"></p>
|
||
<label for="se-smooth">Smoothing <span id="se-smooth-v"></span><span class="se-num"><input id="se-smooth-n" type="number" min="0" max="100" step="1" placeholder="mixed" aria-label="Smoothing, in percent"><span class="se-unit">%</span></span></label>
|
||
<input id="se-smooth" type="range" min="0" max="100" step="1">
|
||
<div class="se-scale"><span>Cut</span><span>Round</span><span>Squircle</span></div>
|
||
<div class="se-row"><button id="se-apply-all" class="se-btn" type="button">Apply to all corners</button></div>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Grid</legend>
|
||
<label>Show grid <input id="se-show-grid" type="checkbox" checked></label>
|
||
<label>Snap to grid <input id="se-snap" type="checkbox" checked></label>
|
||
<label>Step
|
||
<select id="se-step">
|
||
<option>2.5</option><option selected>5</option><option>10</option><option>12.5</option><option>25</option>
|
||
</select>
|
||
</label>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Polygon / star</legend>
|
||
<label>Sides <input id="se-sides" type="number" min="3" max="24" value="6"></label>
|
||
<label>Inner radius % <input id="se-inner" type="number" min="10" max="100" value="100"></label>
|
||
<label>Rotation° <input id="se-rot" type="number" step="15" value="0"></label>
|
||
<label>Tip corner size <input id="se-gen-r" type="number" min="0" max="50" step="1" value="14"></label>
|
||
<label>Inner corner size <input id="se-gen-ir" type="number" min="0" max="50" step="1" value="6"></label>
|
||
<label>Smoothing % <input id="se-gen-s" type="number" min="0" max="100" step="5" value="50"></label>
|
||
<div class="se-row"><button id="se-gen" class="se-btn" type="button">Generate</button></div>
|
||
</fieldset>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<h2>Result</h2>
|
||
<div class="se-result">
|
||
<div class="se-previews" aria-hidden="true">
|
||
<svg width="0" height="0"><defs><linearGradient id="se-icon-fill" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0" stop-color="#C1556A"/><stop offset="1" stop-color="#962CA0"/></linearGradient></defs></svg>
|
||
<svg class="se-preview" width="72" height="72" viewBox="0 0 100 100"><path/></svg>
|
||
<svg class="se-preview" width="48" height="48" viewBox="0 0 100 100"><path/></svg>
|
||
<svg class="se-preview" width="32" height="32" viewBox="0 0 100 100"><path/></svg>
|
||
</div>
|
||
<div>
|
||
<textarea id="se-out" readonly aria-label="Path data" spellcheck="false"></textarea>
|
||
<p id="se-warn" class="se-warn" hidden></p>
|
||
<p id="se-note" class="se-note-out" hidden></p>
|
||
<div class="se-actions">
|
||
<a id="se-use" class="btn-filled" href="#">Use in Murine</a>
|
||
<button id="se-share-open" class="btn-outlined" type="button" aria-haspopup="dialog">Share</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<details class="se-docs">
|
||
<summary>Link format</summary>
|
||
<p>An edit link opens an icon in this editor. It's the editor's URL with the icon in its fragment, in one of two forms:</p>
|
||
<ul>
|
||
<li><code>https://www.murinelauncher.app/icon-shapes/#edit=DATA</code>: the icon's JSON (below), compressed with raw
|
||
DEFLATE (no zlib header or checksum: <code>zlib.compressobj(9, zlib.DEFLATED, -15)</code> in Python,
|
||
<code>new CompressionStream('deflate-raw')</code> in JavaScript), then base64url-encoded without <code>=</code> padding.
|
||
This is the preferred form: it's what <strong>Share</strong> produces and it keeps links short.</li>
|
||
<li><code>https://www.murinelauncher.app/icon-shapes/#edit-raw=JSON</code>: the same JSON, uncompressed and
|
||
percent-encoded (<code>encodeURIComponent</code> in JavaScript, <code>urllib.parse.quote(json, safe='')</code> in
|
||
Python). Use it when compressing isn't possible.</li>
|
||
</ul>
|
||
<p>Opening either one loads the icon (the previous one stays a step back in Undo); <strong>Share</strong> then gives the
|
||
short link and the link that sends the shape to Murine. A link that doesn't follow the format below is ignored, and
|
||
the editor keeps the icon it had.</p>
|
||
<h3>Checking a link</h3>
|
||
<p>A link written by hand or by a script is worth opening before it's shared: the canvas shows the shape, and anything the
|
||
launcher can't use (shapes that don't touch, crossing edges) is reported in red under <strong>Result</strong>, which
|
||
also disables <strong>Use in Murine</strong>. Other messages are just information: a corner whose size shrinks its
|
||
neighbours, for instance, is fine if the shape looks as intended. Small fixes can be made right there. <strong>Share</strong> then has the finished links: the address of its <strong>Edit link</strong> entry is the
|
||
compressed edit link (clicking the entry copies it), and <strong>Use in Murine</strong> points to the import link.</p>
|
||
<h3>The JSON</h3>
|
||
<pre><code>{"shapes": [
|
||
{"pts": [{"x": 50, "y": 4, "r": 12, "s": 0.6}, {"x": 97.6, "y": 38.5, "r": 12, "s": 0.6}, …],
|
||
"mirror": "none"}
|
||
]}</code></pre>
|
||
<ul>
|
||
<li>The icon is drawn in a 100 × 100 box: <code>x</code> grows to the right, <code>y</code> grows <em>down</em>, and
|
||
<code>(0, 0)</code> is the top-left corner. The launcher scales this box to the icon's size, so the shape should fill it
|
||
and stay inside it.</li>
|
||
<li><code>shapes</code>: 1 to 20 shapes. The icon is all of them together, so they must overlap or touch and make a single
|
||
outline; gaps they enclose are filled in.</li>
|
||
<li><code>pts</code>: the corners of one shape, 3 to 200, in order around it (either direction). Its edges must not cross
|
||
each other.</li>
|
||
<li><code>r</code> (optional, default 0): how far along each edge the corner's rounding reaches, in box units; 0 is a sharp
|
||
corner. Where edges are too short for it, it's reduced automatically (along with its neighbours'), which is fine
|
||
as long as the result looks right.</li>
|
||
<li><code>s</code> (optional, default 0): the rounding's profile, 0 to 1: 0 cuts the corner straight, 0.5 is a circular
|
||
arc, 1 is the smoothest (squircle-like).</li>
|
||
<li><code>mirror</code> (optional, default <code>"none"</code>): <code>"h"</code> adds a copy mirrored left to right
|
||
(around <code>x = 50</code>), <code>"v"</code> one mirrored top to bottom (around <code>y = 50</code>),
|
||
<code>"both"</code> three copies, one per quarter.</li>
|
||
</ul>
|
||
<p>A shape can also be an SVG outline instead of corners, for free-form curves. It can be mixed with corner shapes in the
|
||
same icon, and takes <code>mirror</code> too:</p>
|
||
<pre><code>{"path": "M 50 0 C 77.6 0 100 22.4 100 50 C 100 77.6 77.6 100 50 100 C 22.4 100 0 77.6 0 50 C 0 22.4 22.4 0 50 0 Z"}</code></pre>
|
||
<ul>
|
||
<li><code>path</code>: one closed outline in the same 100 × 100 box, written in a strict form: it starts with
|
||
<code>M x y</code>, continues with <code>L x y</code> (lines) and <code>C x1 y1 x2 y2 x y</code> (cubic curves) only,
|
||
and ends with <code>Z</code>; absolute coordinates, every command letter and number separated by a single space, no
|
||
commas. Other commands (relative ones, <code>H</code>/<code>V</code>, <code>S</code>, <code>Q</code>, <code>A</code>)
|
||
and holes or extra outlines aren't accepted, and the outline must not cross itself. To use an existing SVG file,
|
||
<strong>Add a shape → Import SVG</strong> converts it, and <strong>Share</strong> then gives the link.</li>
|
||
<li><code>a</code>, <code>x</code>, <code>y</code> (optional): the outline is drawn scaled by <code>a</code> (default 1)
|
||
and then moved by <code>x</code>, <code>y</code> (default 0).</li>
|
||
<li><code>ang</code> (optional, default 0): a turn in degrees, clockwise, around the outline's centre.</li>
|
||
</ul>
|
||
<p>Next to <code>shapes</code>, an optional <code>whole</code> transforms the icon as a whole, after its shapes are
|
||
merged (mirrored copies included), without changing them: <code>{"shapes": [...], "whole": {"a": 0.9, "x": 5, "y": 5,
|
||
"ang": 30, "flip": true}}</code>. In order: <code>flip</code> (optional, default false) turns the icon over left to
|
||
right within the 100 × 100 box; <code>a</code> (default 1) scales it and <code>x</code>, <code>y</code> (default 0)
|
||
move it; <code>ang</code> (default 0) turns it clockwise, in degrees, around the middle of where the box now is.
|
||
In the editor it's the <strong>Global</strong> card.</p>
|
||
<h3>Example</h3>
|
||
<p>A rounded pentagon: <code>{"shapes":[{"pts":[{"x":50,"y":4,"r":12,"s":0.6},{"x":97.6,"y":38.5,"r":12,"s":0.6},{"x":79.4,"y":94.5,"r":12,"s":0.6},{"x":20.6,"y":94.5,"r":12,"s":0.6},{"x":2.4,"y":38.5,"r":12,"s":0.6}]}]}</code></p>
|
||
<ul class="se-docs-links">
|
||
<li>Compressed: <a href="#edit=q1YqzkgsSC1WsoquVioogdAVSlamBnoGOkqVSlYmILpIycrQSEcJKGugZ1arA1Zhaa5nBlZhbKFnil2JuaWeCViJpQkuJUYGUFPwKIEags2eWCAEAA">https://www.murinelauncher.app/icon-shapes/#edit=q1YqzkgsSC1WsoquVioogdAVSlamBnoGOkqVSlYmILpIycrQSEcJKGugZ1arA1Zhaa5nBlZhbKFnil2JuaWeCViJpQkuJUYGUFPwKIEags2eWCAEAA</a></li>
|
||
<li>Raw: <a href="#edit-raw=%7B%22shapes%22%3A%5B%7B%22pts%22%3A%5B%7B%22x%22%3A50.0%2C%22y%22%3A4.0%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A97.6%2C%22y%22%3A38.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A79.4%2C%22y%22%3A94.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A20.6%2C%22y%22%3A94.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A2.4%2C%22y%22%3A38.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%5D%7D%5D%7D">https://www.murinelauncher.app/icon-shapes/#edit-raw=%7B%22shapes%22%3A%5B%7B%22pts%22%3A%5B%7B%22x%22%3A50.0%2C%22y%22%3A4.0%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A97.6%2C%22y%22%3A38.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A79.4%2C%22y%22%3A94.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A20.6%2C%22y%22%3A94.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%2C%7B%22x%22%3A2.4%2C%22y%22%3A38.5%2C%22r%22%3A12%2C%22s%22%3A0.6%7D%5D%7D%5D%7D</a></li>
|
||
</ul>
|
||
</details>
|
||
|
||
<div class="contact-modal-overlay" id="se-share" aria-hidden="true">
|
||
<div class="contact-modal" role="dialog" aria-modal="true" aria-labelledby="se-share-title">
|
||
<div class="contact-modal-header">
|
||
<h2 id="se-share-title">Share shape</h2>
|
||
<button class="contact-modal-close" type="button" aria-label="Close" data-close>
|
||
<span class="material-symbols-outlined"></span> <!-- close -->
|
||
</button>
|
||
</div>
|
||
<p class="contact-modal-desc">Copy the shape in the form you need.</p>
|
||
<div class="contact-cards">
|
||
<!-- a link, so its address is the edit link itself (copy link address works too); a click copies it -->
|
||
<a class="contact-card se-share-card" id="se-share-edit" href="#" data-copy="edit">
|
||
<span class="contact-card-icon"><span class="material-symbols-outlined"></span></span> <!-- palette -->
|
||
<span class="contact-card-info"><span class="se-card-title">Edit link</span><span class="se-card-sub">Reopen this shape in the editor, on any device</span></span>
|
||
</a>
|
||
<button class="contact-card se-share-card" type="button" data-copy="import" id="se-share-import">
|
||
<span class="contact-card-icon"><span class="material-symbols-outlined"></span></span> <!-- smartphone -->
|
||
<span class="contact-card-info"><span class="se-card-title">Import link</span><span class="se-card-sub">Open it on your phone to apply the shape in Murine</span></span>
|
||
</button>
|
||
<button class="contact-card se-share-card" type="button" data-copy="path">
|
||
<span class="contact-card-icon"><span class="material-symbols-outlined"></span></span> <!-- code -->
|
||
<span class="contact-card-info"><span class="se-card-title">Path data</span><span class="se-card-sub">The raw SVG path, 0 to 100 box</span></span>
|
||
</button>
|
||
<button class="contact-card se-share-card" type="button" id="se-share-native" hidden>
|
||
<span class="contact-card-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></span>
|
||
<span class="contact-card-info"><span class="se-card-title">Send edit link</span><span class="se-card-sub">Through another app</span></span>
|
||
</button>
|
||
<!-- typing "devmode" with the dialog open shows this row (see devKeys in shape-editor.js) -->
|
||
<div id="se-dev" class="se-dev" hidden>
|
||
<button class="contact-card se-share-card" type="button" data-copy="json" data-done="Copied">
|
||
<span class="material-symbols-outlined" aria-hidden="true"></span>
|
||
<span class="se-card-title">JSON</span><span class="se-card-sub">Link data</span>
|
||
</button>
|
||
<button class="contact-card se-share-card" type="button" data-copy="yaml" data-done="Copied">
|
||
<span class="material-symbols-outlined" aria-hidden="true"></span>
|
||
<span class="se-card-title">YAML</span><span class="se-card-sub">Preset entry</span>
|
||
</button>
|
||
<button class="contact-card se-share-card" type="button" data-copy="yamljson" data-done="Copied">
|
||
<span class="material-symbols-outlined" aria-hidden="true"></span>
|
||
<span class="se-card-title">YAML-JSON</span><span class="se-card-sub">Inline preset</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contact-modal-overlay" id="se-add" aria-hidden="true">
|
||
<div class="contact-modal" role="dialog" aria-modal="true" aria-labelledby="se-add-title">
|
||
<div class="contact-modal-header">
|
||
<h2 id="se-add-title">Add a shape</h2>
|
||
<button class="contact-modal-close" type="button" aria-label="Close" data-close>
|
||
<span class="material-symbols-outlined"></span> <!-- close -->
|
||
</button>
|
||
</div>
|
||
<p id="se-add-desc" class="contact-modal-desc"></p>
|
||
<div class="se-import">
|
||
<div class="se-row">
|
||
<span id="se-add-lib" class="se-seg" role="group" aria-label="Show in the grid">
|
||
<button type="button" data-lib="presets">Presets</button><button type="button" data-lib="custom" title="Shapes you saved, kept in this browser">Custom</button>
|
||
</span>
|
||
<button id="se-import-btn" class="se-btn" type="button" title="Import the shapes of an SVG file">Import SVG…</button>
|
||
</div>
|
||
<input id="se-import-file" type="file" accept=".svg,image/svg+xml" hidden>
|
||
<p id="se-import-msg" class="se-hint" role="status" hidden></p>
|
||
<div id="se-import-choice" hidden>
|
||
<p id="se-import-count" class="se-hint"></p>
|
||
<div id="se-import-view" class="se-seg" role="group" aria-label="Show in the grid">
|
||
<button type="button" data-view="pieces">Pieces</button><button type="button" data-view="merged">Merged</button>
|
||
</div>
|
||
<div class="se-row">
|
||
<button id="se-import-separate" class="se-btn" type="button">Add all separately</button>
|
||
<button id="se-import-merge" class="se-btn" type="button">Merge all</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="se-add-grid" class="se-presets"></div>
|
||
<div id="se-pick-bar" class="se-pick-bar" hidden>
|
||
<span id="se-pick-count"></span>
|
||
<span class="se-pick-actions">
|
||
<button id="se-pick-clear" class="se-btn" type="button">Cancel</button>
|
||
<button id="se-pick-merge" class="se-btn" type="button" hidden>Merge</button>
|
||
<button id="se-pick-bake" class="se-btn" type="button" hidden>Bake</button>
|
||
<button id="se-pick-add" class="se-btn se-btn-primary" type="button">Add</button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="contact-modal-overlay" id="se-name" aria-hidden="true">
|
||
<div class="contact-modal se-name-modal" role="dialog" aria-modal="true" aria-labelledby="se-name-title">
|
||
<div class="contact-modal-header">
|
||
<h2 id="se-name-title">Save shape</h2>
|
||
<button class="contact-modal-close" type="button" aria-label="Close" data-close>
|
||
<span class="material-symbols-outlined"></span> <!-- close -->
|
||
</button>
|
||
</div>
|
||
<form id="se-name-form">
|
||
<label for="se-name-input" class="se-hint">Name</label>
|
||
<input id="se-name-input" class="se-name-input" type="text" maxlength="40" required autocomplete="off">
|
||
<p id="se-name-msg" class="se-hint" role="status" hidden></p>
|
||
<div class="se-pick-actions">
|
||
<button class="se-btn" type="button" data-close>Cancel</button>
|
||
<button id="se-name-ok" class="se-btn se-btn-primary" type="submit">Save</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<svg id="se-measure" viewBox="0 0 100 100" aria-hidden="true"><path id="se-measure-path"/></svg>
|
||
|
||
<script src="{{ '/assets/js/polygon-clipping.min.js' | relative_url }}?v={{ site.version }}"></script>
|
||
<script id="se-presets-data" type="application/json">{{ site.data.icon_shapes | jsonify }}</script>
|
||
<script src="{{ '/assets/js/shape-editor.js' | relative_url }}?v={{ site.version }}"></script>
|