Some more improvements.

This commit is contained in:
Yaro Kasear 2025-12-16 10:48:21 -06:00
parent 8d5ddca229
commit 9d22c55aba

View file

@ -17,13 +17,6 @@
margin: 0 auto; margin: 0 auto;
} }
#toolBar {
top: 10px;
transform: translateX(-50%);
z-index: 10000;
max-width: calc(100% - 20px);
}
#toolBar::-webkit-scrollbar { #toolBar::-webkit-scrollbar {
height: 8px; height: 8px;
} }
@ -39,142 +32,149 @@
pointer-events: none; pointer-events: none;
inset: 0; inset: 0;
} }
#toolBar {
margin: 0 auto;
}
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<div class="container"> <div class="container">
<div id="toolBar"
class="btn-toolbar bg-light border border-bottom-0 rounded-bottom-0 border-secondary-subtle rounded p-1 align-items-center flex-nowrap overflow-auto">
<div class="input-group input-group-sm w-auto flex-nowrap">
<span class="input-group-text">Grid Size:</span>
<input type="number" min="1" value="25" name="gridSize" id="gridSize" class="form-control form-control-sm">
<span class="input-group-text">Fill Opacity:</span>
<input type="number" min="0" max="1" step=".01" name="fillOpacity" id="fillOpacity"
class="form-control form-control-sm">
</div>
<div class="vr mx-1"></div>
<input type="color" class="form-control form-control-sm form-control-color" id="color">
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" id="outline" name="tool" checked>
<label for="outline"
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" class="bi bi-square"
viewBox="0 0 16 16">
<path
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" />
</svg>
</label>
<input type="radio" class="btn-check" id="filled" name="tool">
<label for="filled"
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"
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" />
</svg>
</label>
<input type="radio" class="btn-check" id="outlineEllipse" name="tool">
<label for="outlineEllipse"
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" 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" />
</svg>
</label>
<input type="radio" class="btn-check" id="filledEllipse" name="tool">
<label for="filledEllipse"
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"
class="bi bi-circle-fill" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" />
</svg>
</label>
<input type="radio" class="btn-check" id="line" name="tool">
<label for="line"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" name="gridType" id="noGrid" checked>
<label for="noGrid"
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" class="bi bi-border"
viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="horizontalGrid">
<label for="horizontalGrid"
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"
class="bi bi-border-center" viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="verticalGrid">
<label for="verticalGrid"
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"
class="bi bi-border-middle" viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="fullGrid">
<label for="fullGrid"
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"
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" />
</svg>
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center" id="export">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-download" viewBox="0 0 16 16">
<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" />
<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" />
</svg>
</button>
<input type="file" id="import" accept="application/json" class="d-none">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="importButton">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload"
viewBox="0 0 16 16">
<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" />
<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" />
</svg>
</button>
<button type="button"
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center" id="clear">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg"
viewBox="0 0 16 16">
<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" />
</svg>
</button>
</div>
</div>
<div id="gridWrap"> <div id="gridWrap">
<div id="grid" class="position-relative overflow-hidden"> <div id="grid" class="position-relative overflow-hidden">
<div id="toolBar"
class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1 align-items-center flex-nowrap overflow-auto">
<div class="input-group input-group-sm w-auto">
<span class="input-group-text">Grid Size:</span>
<input type="number" min="1" value="25" name="gridSize" id="gridSize" class="form-control form-control-sm">
</div>
<div class="vr mx-1"></div>
<input type="color" class="form-control form-control-sm form-control-color" id="color">
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" id="outline" name="tool" checked>
<label for="outline"
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"
class="bi bi-square" viewBox="0 0 16 16">
<path
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" />
</svg>
</label>
<input type="radio" class="btn-check" id="filled" name="tool">
<label for="filled"
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"
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" />
</svg>
</label>
<input type="radio" class="btn-check" id="outlineEllipse" name="tool">
<label for="outlineEllipse"
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"
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" />
</svg>
</label>
<input type="radio" class="btn-check" id="filledEllipse" name="tool">
<label for="filledEllipse"
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"
class="bi bi-circle-fill" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" />
</svg>
</label>
<input type="radio" class="btn-check" id="line" name="tool">
<label for="line"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" name="gridType" id="noGrid" checked>
<label for="noGrid"
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"
class="bi bi-border" viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="horizontalGrid">
<label for="horizontalGrid"
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"
class="bi bi-border-center" viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="verticalGrid">
<label for="verticalGrid"
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"
class="bi bi-border-middle" viewBox="0 0 16 16">
<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" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="fullGrid">
<label for="fullGrid"
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"
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" />
</svg>
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="export">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-download" viewBox="0 0 16 16">
<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" />
<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" />
</svg>
</button>
<input type="file" id="import" accept="application/json" class="d-none">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="importButton">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-upload" viewBox="0 0 16 16">
<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" />
<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" />
</svg>
</button>
<button type="button"
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center"
id="clear">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-x-lg" viewBox="0 0 16 16">
<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" />
</svg>
</button>
</div>
</div>
<span id="dot" class="position-absolute p-0 m-0 d-none"> <span id="dot" class="position-absolute p-0 m-0 d-none">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" id="dotSVG">
id="dotSVG">
<circle cx="16" cy="16" r="4" fill="black" /> <circle cx="16" cy="16" r="4" fill="black" />
</svg> </svg>
</span> </span>
@ -201,6 +201,7 @@ 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'); const gridWrapEl = document.getElementById('gridWrap');
const toolBarEl = document.getElementById('toolBar');
let doc = loadDoc(); let doc = loadDoc();
let gridSize = Number(doc.gridSize) || 25; let gridSize = Number(doc.gridSize) || 25;
@ -234,10 +235,27 @@ resizeAndSetupCanvas();
window.addEventListener('resize', resizeAndSetupCanvas); window.addEventListener('resize', resizeAndSetupCanvas);
setGrid(); setGrid();
scheduleSnappedGridSize();
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
function sanitizeShapes(list) {
return list.filter(s => {
if (!s || typeof s !== 'object') return false;
if (!['rect','ellipse','line'].include(s.type)) return false;
if (!s.color) s.color = '#000000';
if (s.type === 'line') {
return ['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]));
} else {
return ['x','y','w','h'].every(k => isFiniteNum(s[k]));
}
});
}
function loadDoc() { function loadDoc() {
try { return JSON.parse(localStorage.getItem("gridDoc")) || DEFAULT_DOC; } try { return JSON.parse(localStorage.getItem("gridDoc")) || structuredClone(DEFAULT_DOC); }
catch { return DEFAULT_DOC; } catch { return structuredClone(DEFAULT_DOC); }
} }
function saveDoc(nextDoc = doc) { function saveDoc(nextDoc = doc) {
@ -267,7 +285,10 @@ function applySnappedGridSize() {
gridEl.style.width = `${snappedW}px`; gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`; gridEl.style.height = `${snappedH}px`;
requestAnimationFrame(resizeAndSetupCanvas); toolBarEl.style.width = `${snappedW}px`;
gridEl.getBoundingClientRect();
resizeAndSetupCanvas();
} }
function scheduleSnappedGridSize() { function scheduleSnappedGridSize() {
@ -280,8 +301,7 @@ function applyGridSize(newSize) {
if (!Number.isFinite(n) || n < 1) return; if (!Number.isFinite(n) || n < 1) return;
gridSize = n; gridSize = n;
doc.gridSize = gridSize; saveDoc({ ...doc, shapes, gridSize });
saveDoc({ ...doc, shapes });
dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@ -324,11 +344,9 @@ function setActiveType(typeId) {
function snapToGrid(x, y) { function snapToGrid(x, y) {
/* /*
For portability, we do not allow pixel coordinates in the data model Shapes are stored in grid units (document units), not pixels.
and only use pixels for rendering. We display both spaces on the screen 1 unit renders as gridSize pixels, so changing gridSize rescales (zooms) the whole drawing.
to ensure no matter the mode, the user is reasoning in the same two Grid modes only affect snapping/visuals; storage is always in document units for portability.
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();
@ -362,7 +380,9 @@ function snapToGrid(x, y) {
ix, ix,
iy, iy,
x: snapX, x: snapX,
y: snapY y: snapY,
localX,
localY
}; };
} }
@ -401,10 +421,15 @@ function normalizeLine(shape) {
function resizeAndSetupCanvas() { function resizeAndSetupCanvas() {
dpr = window.devicePixelRatio || 1; dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect();
canvasEl.width = rect.width * dpr; const w = gridEl.clientWidth;
canvasEl.height = rect.height * dpr; const h = gridEl.clientHeight;
canvasEl.width = Math.round(w * dpr);
canvasEl.height = Math.round(h * dpr);
canvasEl.style.width = `${w}px`;
canvasEl.style.height = `${h}px`;
ctx = canvasEl.getContext('2d'); ctx = canvasEl.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@ -538,6 +563,7 @@ document.querySelectorAll('input[name="gridType"]').forEach(input => {
localStorage.setItem('gridType', input.id); localStorage.setItem('gridType', input.id);
} }
setGrid(); setGrid();
redrawAll();
}); });
}); });
@ -563,7 +589,7 @@ importEl.addEventListener('change', (e) => {
const loadedShapes = Array.isArray(data) ? data : data.shapes; const loadedShapes = Array.isArray(data) ? data : data.shapes;
if (!Array.isArray(loadedShapes)) return; if (!Array.isArray(loadedShapes)) return;
shapes = loadedShapes; shapes = sanitizeShapes(loadedShapes);
doc = { doc = {
version: Number(data?.version) || 1, version: Number(data?.version) || 1,
@ -571,7 +597,7 @@ importEl.addEventListener('change', (e) => {
shapes shapes
}; };
saveDoc(data); saveDoc(doc);
redrawAll(); redrawAll();
} catch { } catch {
toastMessage('Failed to load data from JSON file.', 'danger'); toastMessage('Failed to load data from JSON file.', 'danger');
@ -598,9 +624,7 @@ exportEl.addEventListener('click', () => {
clearEl.addEventListener('click', () => { clearEl.addEventListener('click', () => {
gridSize = 25; gridSize = 25;
shapes = []; shapes = [];
doc.gridSize = gridSize; saveDoc({...doc, shapes, gridSize});
doc.shapes = shapes;
saveDoc(doc);
gridSizeEl.value = 25; gridSizeEl.value = 25;
applyGridSize(25); applyGridSize(25);
redrawAll(); redrawAll();
@ -621,7 +645,7 @@ document.addEventListener('keydown', (e) => {
e.preventDefault(); e.preventDefault();
if (shapes.length > 0) { if (shapes.length > 0) {
shapes.pop(); shapes.pop();
saveShapes(); saveDoc({ ...doc, shapes, gridSize });
redrawAll(); redrawAll();
} }
} }
@ -632,10 +656,20 @@ document.addEventListener('keydown', (e) => {
} }
}); });
gridEl.addEventListener('pointercancel', () => {
currentShape = null;
redrawAll();
});
gridEl.addEventListener('lostpointercapture', () => {
currentShape = null;
redrawAll();
});
gridEl.addEventListener('pointermove', (e) => { gridEl.addEventListener('pointermove', (e) => {
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, localX, localY } = snapToGrid(e.clientX, e.clientY);
const renderX = snapX - dotSize / 2; const renderX = snapX - dotSize / 2;
@ -649,7 +683,11 @@ gridEl.addEventListener('pointermove', (e) => {
dotEl.style.left = `${renderX}px`; dotEl.style.left = `${renderX}px`;
} }
coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px) )`; if (getActiveType() == 'noGrid') {
coordsEl.innerText = `(px x=${Math.round(localX)} y=${Math.round(localY)})`;
} else {
coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px))`;
}
if (currentShape) { if (currentShape) {
const tool = currentShape.tool; const tool = currentShape.tool;
@ -725,7 +763,7 @@ gridEl.addEventListener('pointerdown', (e) => {
x2: snapX, x2: snapX,
y2: snapY, y2: snapY,
color: selectedColor, color: selectedColor,
fill: document.getElementById('filled').checked fill: (tool === 'filled' || tool === 'filledEllipse')
}; };
} else if (tool === 'outlineEllipse' || tool === 'filledEllipse') { } else if (tool === 'outlineEllipse' || tool === 'filledEllipse') {
currentShape = { currentShape = {
@ -773,8 +811,7 @@ window.addEventListener('pointerup', (e) => {
if (finalShape) { if (finalShape) {
shapes.push(finalShape); shapes.push(finalShape);
doc.shapes = shapes; saveDoc({...doc, shapes, gridSize});
saveDoc(doc);
} }
clearCanvas(); clearCanvas();