153 lines
6.4 KiB
Plaintext
153 lines
6.4 KiB
Plaintext
@model CharacterAvatarCropViewModel
|
|
@{
|
|
ViewData["Title"] = "Create Avatar";
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-action="Details" asp-route-id="@Model.CharacterID">@Model.CharacterName</a>
|
|
<span>Create Avatar</span>
|
|
</nav>
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Character Avatar</p>
|
|
<h1>Create Avatar</h1>
|
|
<p class="lead-text">Choose a square crop. The original gallery image will be preserved.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (TempData["ImageError"] is string imageError)
|
|
{
|
|
<div class="alert alert-warning">@imageError</div>
|
|
}
|
|
|
|
<form asp-action="SaveAvatar" method="post" class="edit-panel character-avatar-crop-form" data-avatar-crop-form>
|
|
<input asp-for="CharacterID" type="hidden" />
|
|
<input asp-for="CharacterImageID" type="hidden" />
|
|
<input asp-for="CropX" type="hidden" data-crop-x />
|
|
<input asp-for="CropY" type="hidden" data-crop-y />
|
|
<input asp-for="CropSize" type="hidden" data-crop-size />
|
|
|
|
<div class="character-avatar-crop-layout">
|
|
<div class="character-avatar-crop-stage" data-crop-stage>
|
|
<img src="@Model.ImagePath" alt="" data-crop-image />
|
|
<div class="character-avatar-crop-box" data-crop-box>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
<div class="character-avatar-crop-side">
|
|
<p class="eyebrow">Preview</p>
|
|
<div class="character-avatar-preview" data-avatar-preview>
|
|
<img src="@Model.ImagePath" alt="" data-preview-image />
|
|
</div>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Caption))
|
|
{
|
|
<p class="muted">@Model.Caption</p>
|
|
}
|
|
<label class="form-label" for="cropZoom">Crop size</label>
|
|
<input class="form-range" id="cropZoom" type="range" min="20" max="100" value="70" data-crop-size-slider />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-row mt-3">
|
|
<button class="btn btn-primary" type="submit">Save avatar</button>
|
|
<a class="btn btn-outline-secondary" asp-action="Details" asp-route-id="@Model.CharacterID">Cancel</a>
|
|
</div>
|
|
</form>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
(() => {
|
|
const form = document.querySelector("[data-avatar-crop-form]");
|
|
if (!form) return;
|
|
|
|
const stage = form.querySelector("[data-crop-stage]");
|
|
const image = form.querySelector("[data-crop-image]");
|
|
const box = form.querySelector("[data-crop-box]");
|
|
const slider = form.querySelector("[data-crop-size-slider]");
|
|
const preview = form.querySelector("[data-avatar-preview]");
|
|
const previewImage = form.querySelector("[data-preview-image]");
|
|
const cropX = form.querySelector("[data-crop-x]");
|
|
const cropY = form.querySelector("[data-crop-y]");
|
|
const cropSize = form.querySelector("[data-crop-size]");
|
|
let crop = { x: 0, y: 0, size: 0 };
|
|
let dragging = false;
|
|
let dragOffset = { x: 0, y: 0 };
|
|
|
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
|
|
const imageRect = () => {
|
|
const stageRect = stage.getBoundingClientRect();
|
|
const imgRect = image.getBoundingClientRect();
|
|
return {
|
|
x: imgRect.left - stageRect.left,
|
|
y: imgRect.top - stageRect.top,
|
|
width: imgRect.width,
|
|
height: imgRect.height
|
|
};
|
|
};
|
|
|
|
const sync = () => {
|
|
const rect = imageRect();
|
|
const minSide = Math.min(rect.width, rect.height);
|
|
const percent = Number(slider.value) / 100;
|
|
crop.size = Math.max(24, minSide * percent);
|
|
crop.x = clamp(crop.x, rect.x, rect.x + rect.width - crop.size);
|
|
crop.y = clamp(crop.y, rect.y, rect.y + rect.height - crop.size);
|
|
|
|
box.style.width = `${crop.size}px`;
|
|
box.style.height = `${crop.size}px`;
|
|
box.style.left = `${crop.x}px`;
|
|
box.style.top = `${crop.y}px`;
|
|
|
|
const naturalScaleX = image.naturalWidth / rect.width;
|
|
const naturalScaleY = image.naturalHeight / rect.height;
|
|
cropX.value = Math.round((crop.x - rect.x) * naturalScaleX);
|
|
cropY.value = Math.round((crop.y - rect.y) * naturalScaleY);
|
|
cropSize.value = Math.round(crop.size * Math.min(naturalScaleX, naturalScaleY));
|
|
|
|
const previewSize = preview.clientWidth || 180;
|
|
const scale = previewSize / crop.size;
|
|
previewImage.style.width = `${rect.width * scale}px`;
|
|
previewImage.style.height = `${rect.height * scale}px`;
|
|
previewImage.style.transform = `translate(${-(crop.x - rect.x) * scale}px, ${-(crop.y - rect.y) * scale}px)`;
|
|
};
|
|
|
|
const resetCrop = () => {
|
|
const rect = imageRect();
|
|
const minSide = Math.min(rect.width, rect.height);
|
|
crop.size = minSide * (Number(slider.value) / 100);
|
|
crop.x = rect.x + (rect.width - crop.size) / 2;
|
|
crop.y = rect.y + (rect.height - crop.size) / 2;
|
|
sync();
|
|
};
|
|
|
|
image.addEventListener("load", resetCrop);
|
|
if (image.complete) window.requestAnimationFrame(resetCrop);
|
|
window.addEventListener("resize", resetCrop);
|
|
slider.addEventListener("input", sync);
|
|
|
|
box.addEventListener("pointerdown", (event) => {
|
|
dragging = true;
|
|
box.setPointerCapture(event.pointerId);
|
|
dragOffset = { x: event.offsetX, y: event.offsetY };
|
|
});
|
|
|
|
box.addEventListener("pointermove", (event) => {
|
|
if (!dragging) return;
|
|
const stageRect = stage.getBoundingClientRect();
|
|
const rect = imageRect();
|
|
crop.x = clamp(event.clientX - stageRect.left - dragOffset.x, rect.x, rect.x + rect.width - crop.size);
|
|
crop.y = clamp(event.clientY - stageRect.top - dragOffset.y, rect.y, rect.y + rect.height - crop.size);
|
|
sync();
|
|
});
|
|
|
|
box.addEventListener("pointerup", () => {
|
|
dragging = false;
|
|
});
|
|
|
|
form.addEventListener("submit", sync);
|
|
})();
|
|
</script>
|
|
}
|