Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
11 kB · 360 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361import { Component } from "/js/components/component.js";
const MIN_SCALE = 1;const MAX_SCALE = 5;
const SHAPES = new Set(["circle", "square", "rounded-square"]);const ROUNDED_SQUARE_RADIUS = 8;
// Claude wrote thisclass ImageCropper extends Component { connectedCallback() { if (this.initialized) { return; } this._scale = 1; this._offsetX = 0; this._offsetY = 0; this._dragging = false; this._dragStartX = 0; this._dragStartY = 0; this._dragStartOffsetX = 0; this._dragStartOffsetY = 0; this._img = null;
this.innerHTML = ""; const canvas = document.createElement("canvas"); canvas.className = "image-cropper-canvas"; this.appendChild(canvas); this._canvas = canvas; this._ctx = canvas.getContext("2d") || null;
const sliderContainer = document.createElement("div"); sliderContainer.className = "image-cropper-slider-container"; const slider = document.createElement("input"); slider.type = "range"; slider.className = "image-cropper-slider"; slider.min = "0"; slider.max = "100"; slider.value = "0"; slider.step = "1"; sliderContainer.appendChild(slider); this.appendChild(sliderContainer); this._slider = slider;
this._bindEvents(); this.initialized = true;
if (this.getAttribute("src")) { this.loadImage(this.getAttribute("src")); } }
static get observedAttributes() { return ["src", "aspect-ratio", "shape"]; }
attributeChangedCallback(name, oldValue, newValue) { if (!this.initialized) return; if (name === "src" && newValue !== oldValue) { this.loadImage(newValue); } else if (name === "aspect-ratio" || name === "shape") { this._draw(); } }
get aspectRatio() { return parseFloat(this.getAttribute("aspect-ratio")) || 1; }
get shape() { const attr = this.getAttribute("shape"); return SHAPES.has(attr) ? attr : "square"; }
async loadImage(src) { const img = document.createElement("img"); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = src; }); this._img = img; this._scale = 1; this._offsetX = 0; this._offsetY = 0; this._draw(); this._fitInitialScale(); this._draw(); }
_fitInitialScale() { if (!this._img) return; this._scale = this._getMinScale(); this._offsetX = 0; this._offsetY = 0; this._syncSlider(); }
_getCropRect() { const rect = this._canvas.getBoundingClientRect(); const padding = 20; const maxWidth = rect.width - padding * 2; const maxHeight = rect.height - padding * 2; const aspectRatio = this.aspectRatio;
let cropWidth, cropHeight; if (maxWidth / maxHeight > aspectRatio) { cropHeight = maxHeight; cropWidth = cropHeight * aspectRatio; } else { cropWidth = maxWidth; cropHeight = cropWidth / aspectRatio; }
return { x: (rect.width - cropWidth) / 2, y: (rect.height - cropHeight) / 2, width: cropWidth, height: cropHeight, }; }
_clampOffset() { if (!this._img) return; const rect = this._canvas.getBoundingClientRect(); const padding = 20; const maxWidth = rect.width - padding * 2; const maxHeight = rect.height - padding * 2; const aspectRatio = this.aspectRatio;
let cropWidth, cropHeight; if (maxWidth / maxHeight > aspectRatio) { cropHeight = maxHeight; cropWidth = cropHeight * aspectRatio; } else { cropWidth = maxWidth; cropHeight = cropWidth / aspectRatio; }
const imgWidth = this._img.width * this._scale; const imgHeight = this._img.height * this._scale;
const maxOffsetX = Math.max(0, (imgWidth - cropWidth) / 2); const maxOffsetY = Math.max(0, (imgHeight - cropHeight) / 2); this._offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, this._offsetX)); this._offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, this._offsetY)); }
_draw() { if (!this._ctx) return; const canvas = this._canvas; const ctx = this._ctx; const rect = this._canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; ctx.scale(dpr, dpr);
const displayWidth = rect.width; const displayHeight = rect.height;
ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, displayWidth, displayHeight);
if (!this._img) return;
const cropRect = this._getCropRect(); const cropX = cropRect.x; const cropY = cropRect.y; const cropWidth = cropRect.width; const cropHeight = cropRect.height;
const imgWidth = this._img.width * this._scale; const imgHeight = this._img.height * this._scale; const imgX = displayWidth / 2 - imgWidth / 2 + this._offsetX; const imgY = displayHeight / 2 - imgHeight / 2 + this._offsetY;
ctx.drawImage(this._img, imgX, imgY, imgWidth, imgHeight);
// Darken area outside the crop zone ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
const shape = this.shape; ctx.beginPath(); ctx.rect(0, 0, displayWidth, displayHeight); this._traceCropPath(ctx, cropX, cropY, cropWidth, cropHeight, shape); ctx.fill("evenodd");
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"; ctx.lineWidth = 2; ctx.beginPath(); this._traceCropPath(ctx, cropX, cropY, cropWidth, cropHeight, shape); ctx.stroke(); }
_traceCropPath(ctx, x, y, width, height, shape) { if (shape === "circle") { const radius = width / 2; ctx.arc(x + width / 2, y + height / 2, radius, 0, Math.PI * 2); } else if (shape === "rounded-square") { const r = Math.min(ROUNDED_SQUARE_RADIUS, width / 2, height / 2); ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.arcTo(x + width, y, x + width, y + r, r); ctx.lineTo(x + width, y + height - r); ctx.arcTo(x + width, y + height, x + width - r, y + height, r); ctx.lineTo(x + r, y + height); ctx.arcTo(x, y + height, x, y + height - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); } else { ctx.rect(x, y, width, height); } }
_bindEvents() { const canvas = this._canvas;
canvas.addEventListener("pointerdown", (event) => { this._dragging = true; this._dragStartX = event.clientX; this._dragStartY = event.clientY; this._dragStartOffsetX = this._offsetX; this._dragStartOffsetY = this._offsetY; canvas.setPointerCapture(event.pointerId); event.preventDefault(); });
canvas.addEventListener("pointermove", (event) => { if (!this._dragging) return; this._offsetX = this._dragStartOffsetX + (event.clientX - this._dragStartX); this._offsetY = this._dragStartOffsetY + (event.clientY - this._dragStartY); this._clampOffset(); this._draw(); });
canvas.addEventListener("pointerup", () => { this._dragging = false; });
canvas.addEventListener("pointercancel", () => { this._dragging = false; });
this._slider.addEventListener("input", () => { const sliderValue = parseFloat(this._slider.value) / 100; const minScale = this._getMinScale(); this._scale = minScale + sliderValue * (MAX_SCALE - minScale); this._clampOffset(); this._draw(); }); }
_getMinScale() { if (!this._img) return MIN_SCALE; const rect = this._canvas.getBoundingClientRect(); const padding = 20; const maxWidth = rect.width - padding * 2; const maxHeight = rect.height - padding * 2; const aspectRatio = this.aspectRatio;
let cropWidth, cropHeight; if (maxWidth / maxHeight > aspectRatio) { cropHeight = maxHeight; cropWidth = cropHeight * aspectRatio; } else { cropWidth = maxWidth; cropHeight = cropWidth / aspectRatio; }
const minScaleX = cropWidth / this._img.width; const minScaleY = cropHeight / this._img.height; return Math.max(minScaleX, minScaleY); }
_fitMinScale() { const minScale = this._getMinScale(); if (this._scale < minScale) { this._scale = minScale; } }
_syncSlider() { const minScale = this._getMinScale(); const range = MAX_SCALE - minScale; const value = range > 0 ? ((this._scale - minScale) / range) * 100 : 0; this._slider.value = String(Math.round(value)); }
cropImage() { if (!this._img) return null; const rect = this._canvas.getBoundingClientRect(); const padding = 20; const maxWidth = rect.width - padding * 2; const maxHeight = rect.height - padding * 2; const aspectRatio = this.aspectRatio;
let cropWidth, cropHeight; if (maxWidth / maxHeight > aspectRatio) { cropHeight = maxHeight; cropWidth = cropHeight * aspectRatio; } else { cropWidth = maxWidth; cropHeight = cropWidth / aspectRatio; }
const cropX = (rect.width - cropWidth) / 2; const cropY = (rect.height - cropHeight) / 2;
const imgWidth = this._img.width * this._scale; const imgHeight = this._img.height * this._scale; const imgX = rect.width / 2 - imgWidth / 2 + this._offsetX; const imgY = rect.height / 2 - imgHeight / 2 + this._offsetY;
// Calculate source region in image coordinates const srcX = (cropX - imgX) / this._scale; const srcY = (cropY - imgY) / this._scale; const srcWidth = cropWidth / this._scale; const srcHeight = cropHeight / this._scale;
// Output at a reasonable resolution, scaling proportionally const maxOutputDimension = 2000; const scaleFactor = Math.min( 1, maxOutputDimension / srcWidth, maxOutputDimension / srcHeight, ); const outputWidth = Math.round(srcWidth * scaleFactor); const outputHeight = Math.round(srcHeight * scaleFactor);
const outputCanvas = document.createElement("canvas"); outputCanvas.width = outputWidth; outputCanvas.height = outputHeight; const outputCtx = outputCanvas.getContext("2d"); outputCtx.imageSmoothingEnabled = true; outputCtx.imageSmoothingQuality = "high"; outputCtx.fillStyle = "#ffffff"; outputCtx.fillRect(0, 0, outputWidth, outputHeight); outputCtx.drawImage( this._img, srcX, srcY, srcWidth, srcHeight, 0, 0, outputWidth, outputHeight, );
return outputCanvas.toDataURL("image/png"); }}
ImageCropper.register();