Better grid resizing behavior.

This commit is contained in:
Yaro Kasear 2025-12-12 12:10:14 -06:00
parent 3f4aee73a3
commit 0b85715c1e

View file

@ -2,24 +2,21 @@
{% set dot_size = [grid_size * 1.25, 32]|max|int %} {% set dot_size = [grid_size * 1.25, 32]|max|int %}
{% block style %} {% block style %}
:root {
--grid: {{ grid_size }}px; #gridWrap {
width: 100%;
height: 80vh;
position: relative;
} }
#grid { #grid {
position: relative;
cursor: crosshair; cursor: crosshair;
height: 80vh;
width: 100%; width: 100%;
height: 100%;
touch-action: none; touch-action: none;
} }
@supports (height: calc(round(nearest, 80vh, {{ grid_size }}px))) {
#grid {
height: calc(round(nearest, 80vh, var(--grid)) + 1px);
width: calc(round(nearest, 100%, var(--grid)) + 1px);
}
}
#toolBar { #toolBar {
top: 10px; top: 10px;
transform: translateX(-50%); transform: translateX(-50%);
@ -46,143 +43,145 @@
{% block main %} {% block main %}
<div class="container"> <div class="container">
<div id="grid" class="position-relative overflow-hidden"> <div id="gridWrap">
<div id="toolBar" <div id="grid" class="position-relative overflow-hidden">
class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1 align-items-center flex-nowrap overflow-auto"> <div id="toolBar"
<div class="input-group input-group-sm w-auto"> class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1 align-items-center flex-nowrap overflow-auto">
<span class="input-group-text">Grid Size:</span> <div class="input-group input-group-sm w-auto">
<input type="number" min="1" value="{{ grid_size }}" name="gridSize" id="gridSize" class="form-control form-control-sm"> <span class="input-group-text">Grid Size:</span>
</div> <input type="number" min="1" value="{{ grid_size }}" name="gridSize" id="gridSize" class="form-control form-control-sm">
<div class="vr mx-1"></div> </div>
<input type="color" class="form-control form-control-sm form-control-color" id="color"> <div class="vr mx-1"></div>
<div class="vr mx-1"></div> <input type="color" class="form-control form-control-sm form-control-color" id="color">
<div class="btn-group"> <div class="vr mx-1"></div>
<input type="radio" class="btn-check" id="outline" name="tool" checked> <div class="btn-group">
<label for="outline" <input type="radio" class="btn-check" id="outline" name="tool" checked>
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> <label for="outline"
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
class="bi bi-square" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
<path class="bi bi-square" viewBox="0 0 16 16">
d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" /> <path
</svg> d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" />
</label> </svg>
</label>
<input type="radio" class="btn-check" id="filled" name="tool"> <input type="radio" class="btn-check" id="filled" name="tool">
<label for="filled" <label for="filled"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-square-fill" viewBox="0 0 16 16"> class="bi bi-square-fill" viewBox="0 0 16 16">
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" /> <path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" id="outlineEllipse" name="tool"> <input type="radio" class="btn-check" id="outlineEllipse" name="tool">
<label for="outlineEllipse" <label for="outlineEllipse"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-circle" viewBox="0 0 16 16"> class="bi bi-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" /> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" id="filledEllipse" name="tool"> <input type="radio" class="btn-check" id="filledEllipse" name="tool">
<label for="filledEllipse" <label for="filledEllipse"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-circle-fill" viewBox="0 0 16 16"> class="bi bi-circle-fill" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" /> <circle cx="8" cy="8" r="8" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" id="line" name="tool"> <input type="radio" class="btn-check" id="line" name="tool">
<label for="line" <label for="line"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
</label> </label>
</div> </div>
<div class="vr mx-1"></div> <div class="vr mx-1"></div>
<div class="btn-group"> <div class="btn-group">
<input type="radio" class="btn-check" name="gridType" id="noGrid" checked> <input type="radio" class="btn-check" name="gridType" id="noGrid" checked>
<label for="noGrid" <label for="noGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border" viewBox="0 0 16 16"> class="bi bi-border" viewBox="0 0 16 16">
<path <path
d="M0 0h.969v.5H1v.469H.969V1H.5V.969H0zm2.844 1h-.938V0h.938zm1.875 0H3.78V0h.938v1zm1.875 0h-.938V0h.938zm.937 0V.969H7.5V.5h.031V0h.938v.5H8.5v.469h-.031V1zm2.813 0h-.938V0h.938zm1.875 0h-.938V0h.938zm1.875 0h-.938V0h.938zM15.5 1h-.469V.969H15V.5h.031V0H16v.969h-.5zM1 1.906v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM1 3.78v.938H0V3.78zm6.5.938V3.78h1v.938zm7.5 0V3.78h1v.938zM1 5.656v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM.969 8.5H.5v-.031H0V7.53h.5V7.5h.469v.031H1v.938H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm1.875-.031V8.5H7.53v-.031H7.5V7.53h.031V7.5h.938v.031H8.5v.938zm1.875.031h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.406 0h-.469v-.031H15V7.53h.031V7.5h.469v.031h.5v.938h-.5zM0 10.344v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 12.22v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 14.094v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM.969 16H0v-.969h.5V15h.469v.031H1v.469H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm.937 0v-.5H7.5v-.469h.031V15h.938v.031H8.5v.469h-.031v.5zm2.813 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm.937 0v-.5H15v-.469h.031V15h.469v.031h.5V16z" /> d="M0 0h.969v.5H1v.469H.969V1H.5V.969H0zm2.844 1h-.938V0h.938zm1.875 0H3.78V0h.938v1zm1.875 0h-.938V0h.938zm.937 0V.969H7.5V.5h.031V0h.938v.5H8.5v.469h-.031V1zm2.813 0h-.938V0h.938zm1.875 0h-.938V0h.938zm1.875 0h-.938V0h.938zM15.5 1h-.469V.969H15V.5h.031V0H16v.969h-.5zM1 1.906v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM1 3.78v.938H0V3.78zm6.5.938V3.78h1v.938zm7.5 0V3.78h1v.938zM1 5.656v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM.969 8.5H.5v-.031H0V7.53h.5V7.5h.469v.031H1v.938H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm1.875-.031V8.5H7.53v-.031H7.5V7.53h.031V7.5h.938v.031H8.5v.938zm1.875.031h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.406 0h-.469v-.031H15V7.53h.031V7.5h.469v.031h.5v.938h-.5zM0 10.344v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 12.22v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 14.094v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM.969 16H0v-.969h.5V15h.469v.031H1v.469H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm.937 0v-.5H7.5v-.469h.031V15h.938v.031H8.5v.469h-.031v.5zm2.813 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm.937 0v-.5H15v-.469h.031V15h.469v.031h.5V16z" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" name="gridType" id="horizontalGrid"> <input type="radio" class="btn-check" name="gridType" id="horizontalGrid">
<label for="horizontalGrid" <label for="horizontalGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-center" viewBox="0 0 16 16"> class="bi bi-border-center" viewBox="0 0 16 16">
<path <path
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM7.531.969V1h.938V.969H8.5V.5h-.031V0H7.53v.5H7.5v.469zM9.406 1h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM1 4.719V3.78H0v.938h1zm6.5-.938v.938h1V3.78h-1zm7.5 0v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM0 8.5v-1h16v1zm0 .906v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm1.875-.5v.5h.938v-.5H8.5v-.469h-.031V15H7.53v.031H7.5v.469zm1.875.5h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" /> d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM7.531.969V1h.938V.969H8.5V.5h-.031V0H7.53v.5H7.5v.469zM9.406 1h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM1 4.719V3.78H0v.938h1zm6.5-.938v.938h1V3.78h-1zm7.5 0v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM0 8.5v-1h16v1zm0 .906v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm1.875-.5v.5h.938v-.5H8.5v-.469h-.031V15H7.53v.031H7.5v.469zm1.875.5h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" name="gridType" id="verticalGrid"> <input type="radio" class="btn-check" name="gridType" id="verticalGrid">
<label for="verticalGrid" <label for="verticalGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-middle" viewBox="0 0 16 16"> class="bi bi-border-middle" viewBox="0 0 16 16">
<path <path
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM8.5 16h-1V0h1zm.906-15h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm14-.938v.938h1v-.938zM1 4.719V3.78H0v.938h1zm14-.938v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm14-.938v.938h1v-.938zM.5 8.5h.469v-.031H1V7.53H.969V7.5H.5v.031H0v.938h.5zm1.406 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.469v-.031h.5V7.53h-.5V7.5h-.469v.031H15v.938h.031zM0 9.406v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" /> d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM8.5 16h-1V0h1zm.906-15h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm14-.938v.938h1v-.938zM1 4.719V3.78H0v.938h1zm14-.938v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm14-.938v.938h1v-.938zM.5 8.5h.469v-.031H1V7.53H.969V7.5H.5v.031H0v.938h.5zm1.406 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.469v-.031h.5V7.53h-.5V7.5h-.469v.031H15v.938h.031zM0 9.406v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
</svg> </svg>
</label> </label>
<input type="radio" class="btn-check" name="gridType" id="fullGrid"> <input type="radio" class="btn-check" name="gridType" id="fullGrid">
<label for="fullGrid" <label for="fullGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"> class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-all" viewBox="0 0 16 16"> class="bi bi-border-all" viewBox="0 0 16 16">
<path d="M0 0h16v16H0zm1 1v6.5h6.5V1zm7.5 0v6.5H15V1zM15 8.5H8.5V15H15zM7.5 15V8.5H1V15z" /> <path d="M0 0h16v16H0zm1 1v6.5h6.5V1zm7.5 0v6.5H15V1zM15 8.5H8.5V15H15zM7.5 15V8.5H1V15z" />
</svg> </svg>
</label> </label>
</div> </div>
<div class="vr mx-1"></div> <div class="vr mx-1"></div>
<div class="btn-group"> <div class="btn-group">
<button type="button" <button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="export"> id="export">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-download" viewBox="0 0 16 16"> class="bi bi-download" viewBox="0 0 16 16">
<path <path
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" /> d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path <path
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" /> d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
</svg> </svg>
</button> </button>
<input type="file" id="import" accept="application/json" class="d-none"> <input type="file" id="import" accept="application/json" class="d-none">
<button type="button" <button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="importButton"> id="importButton">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-upload" viewBox="0 0 16 16"> class="bi bi-upload" viewBox="0 0 16 16">
<path <path
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" /> d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path <path
d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z" /> d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z" />
</svg> </svg>
</button> </button>
<button type="button" <button type="button"
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center" class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center"
id="clear"> id="clear">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-x-lg" viewBox="0 0 16 16"> class="bi bi-x-lg" viewBox="0 0 16 16">
<path <path
d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" /> d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" />
</svg> </svg>
</button> </button>
</div>
</div> </div>
<span id="dot" class="position-absolute p-0 m-0 d-none">
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}" height="{{ dot_size }}" viewBox="0 0 32 32"
id="dotSVG">
<circle cx="16" cy="16" r="4" fill="black" />
</svg>
</span>
<div id="coords"
class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
<canvas id="overlay" class="position-absolute w-100 h-100"></canvas>
</div> </div>
<span id="dot" class="position-absolute p-0 m-0 d-none">
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}" height="{{ dot_size }}" viewBox="0 0 32 32"
id="dotSVG">
<circle cx="16" cy="16" r="4" fill="black" />
</svg>
</span>
<div id="coords"
class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
<canvas id="overlay" class="position-absolute w-100 h-100"></canvas>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@ -199,6 +198,7 @@ const gridEl = document.getElementById('grid');
const importButtonEl = document.getElementById('importButton'); const importButtonEl = document.getElementById('importButton');
const importEl = document.getElementById('import'); const importEl = document.getElementById('import');
const gridSizeEl = document.getElementById('gridSize'); const gridSizeEl = document.getElementById('gridSize');
const gridWrapEl = document.getElementById('gridWrap');
let gridSize = Number(gridSizeEl.value) || {{ grid_size }}; let gridSize = Number(gridSizeEl.value) || {{ grid_size }};
let dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); let dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@ -210,8 +210,11 @@ let selectedColor;
let currentShape = null; let currentShape = null;
let shapes = loadShapes(); let shapes = loadShapes();
const ro = new ResizeObserver(() => resizeAndSetupCanvas()); let sizingRAF = 0;
ro.observe(gridEl); let lastApplied = { w: 0, h: 0 };
const ro = new ResizeObserver(scheduleSnappedGridSize);
ro.observe(gridWrapEl);
const savedTool = localStorage.getItem('gridTool'); const savedTool = localStorage.getItem('gridTool');
if (savedTool) { if (savedTool) {
@ -228,12 +231,43 @@ window.addEventListener('resize', resizeAndSetupCanvas);
setGrid(); setGrid();
function snapDown(n, step) {
return Math.floor(n / step) * step;
}
function applySnappedGridSize() {
sizingRAF = 0;
const grid = gridSize;
if (!Number.isFinite(grid) || grid < 1) return;
const w = gridWrapEl.clientWidth;
const h = gridWrapEl.clientHeight;
const snappedW = snapDown(w, grid);
const snappedH = snapDown(h, grid);
if (snappedW === lastApplied.w && snappedH === lastApplied.h) return;
lastApplied = { w: snappedW, h: snappedH };
gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`;
resizeAndSetupCanvas();
}
function scheduleSnappedGridSize() {
if (sizingRAF) return;
sizingRAF = requestAnimationFrame(applySnappedGridSize);
}
function applyGridSize(newSize) { function applyGridSize(newSize) {
const n = Number(newSize); const n = Number(newSize);
if (!Number.isFinite(n) || n < 1) return; if (!Number.isFinite(n) || n < 1) return;
gridSize = n; gridSize = n;
document.documentElement.style.setProperty('--grid', `${gridSize}px`); snapSizeToGrid();
dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@ -241,7 +275,7 @@ function applyGridSize(newSize) {
dotSVGEl.setAttribute('height', dotSize); dotSVGEl.setAttribute('height', dotSize);
setGrid(); setGrid();
resizeAndSetupCanvas(); scheduleSnappedGridSize();
} }
function pxToGrid(v) { function pxToGrid(v) {
@ -274,7 +308,29 @@ function setActiveType(typeId) {
} }
} }
function snapSizeToGrid() {
const grid = gridSize;
const rect = gridEl.getBoundingClientRect();
const targetW = rect.width;
const targetH = rect.height;
const snappedW = Math.floor(targetW / grid) * grid + 1;
const snappedH = Math.floor(targetH / grid) * grid + 1;
gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`;
}
function snapToGrid(x, y) { function snapToGrid(x, y) {
/*
For portability, we do not allow pixel coordinates in the data model
and only use pixels for rendering. We display both spaces on the screen
to ensure no matter the mode, the user is reasoning in the same two
coordinate spaces as they need. Thus, snapping will happen even if
the tool doesn't use it.
*/
const rect = gridEl.getBoundingClientRect(); const rect = gridEl.getBoundingClientRect();
const clampedX = Math.min(Math.max(x, rect.left), rect.right); const clampedX = Math.min(Math.max(x, rect.left), rect.right);
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom); const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
@ -344,6 +400,7 @@ function normalizeLine(shape) {
} }
function resizeAndSetupCanvas() { function resizeAndSetupCanvas() {
snapSizeToGrid();
dpr = window.devicePixelRatio || 1; dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect(); const rect = canvasEl.getBoundingClientRect();
@ -581,9 +638,6 @@ document.addEventListener('keydown', (e) => {
}); });
gridEl.addEventListener('pointermove', (e) => { gridEl.addEventListener('pointermove', (e) => {
// Note to certain minds that keep thinking we want to mix grid and pixel coordinates.
// No. No we do not. Pixels are not portable. Stop it.
if (!ctx) return; if (!ctx) return;
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY); const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);