diff --git a/index.html b/index.html new file mode 100644 index 0000000..569e0df --- /dev/null +++ b/index.html @@ -0,0 +1,235 @@ + + + + + + draw + + + +
+ +
+ + + +
+ + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..a85a41e --- /dev/null +++ b/main.js @@ -0,0 +1,1376 @@ +import { getStroke } from "perfect-freehand"; + +const canvas = document.getElementById("canvas"); +const ctx = canvas.getContext("2d"); + +const hud = document.getElementById("hud"); +const hudTop = document.querySelector(".hud-top"); +const hudDragHandle = document.getElementById("hudDragHandle"); +const hudHoverGap = document.getElementById("hudHoverGap"); + +const toggleTheme = document.getElementById("toggleTheme"); +const cycleCursor = document.getElementById("cycleCursor"); +const cursorDot = document.getElementById("cursorDot"); + +const size = document.getElementById("size"); +const sizeRange = document.getElementById("sizeRange"); +const thinning = document.getElementById("thinning"); +const thinningRange = document.getElementById("thinningRange"); +const smoothing = document.getElementById("smoothing"); +const smoothingRange = document.getElementById("smoothingRange"); +const streamline = document.getElementById("streamline"); +const streamlineRange = document.getElementById("streamlineRange"); +const pressureMode = document.getElementById("pressureMode"); +const eraser = document.getElementById("eraser"); +const strokeColor = document.getElementById("strokeColor"); +const brushPreview = document.getElementById("brushPreview"); +const colorA = document.getElementById("colorA"); +const colorB = document.getElementById("colorB"); +const gradientMix = document.getElementById("gradientMix"); +const gradientMixRange = document.getElementById("gradientMixRange"); +const gradientMode = document.getElementById("gradientMode"); + +const guidesOn = document.getElementById("guidesOn"); +const guidesMode = document.getElementById("guidesMode"); +const guidesSpacing = document.getElementById("guidesSpacing"); +const guidesSpacingRange = document.getElementById("guidesSpacingRange"); +const guidesOpacity = document.getElementById("guidesOpacity"); +const guidesOpacityRange = document.getElementById("guidesOpacityRange"); +const guidesSnap = document.getElementById("guidesSnap"); +const resetGuides = document.getElementById("resetGuides"); + +const clearBtn = document.getElementById("clear"); +const undoBtn = document.getElementById("undo"); +const redoBtn = document.getElementById("redo"); +const saveBtn = document.getElementById("save"); + +const gradientToggle = document.getElementById("gradientToggle"); +const gradientPanel = document.getElementById("gradientPanel"); +const guidesToggle = document.getElementById("guidesToggle"); +const guidesPanel = document.getElementById("guidesPanel"); + +const HUD_POS_KEY = "pfh-hud-pos"; +const SETTINGS_KEY = "pfh-settings"; +const THEME_KEY = "pfh-theme"; +const UI_KEY = "pfh-ui"; +const CURSOR_KEY = "pfh-cursor"; + +const INTERACTIVE = 'button,input,select,textarea,label,[role="button"],[tabindex]'; +const HOVER_PAD = 18; +const EXPAND_DELAY = 10; +const COLLAPSE_DELAY = 10; +const AUTO_COLLAPSE_DELAY = 4800; +const SNAP_PAD = 26; +const VIEWPORT_PAD = 8; +const LONG_PRESS_DELAY = 220; +const LONG_PRESS_MOVE_TOLERANCE = 8; +const COLLAPSE_GRACE_MS = 180; +const cursorOrder = ["default", "crosshair", "dot"]; + +let dpr = window.devicePixelRatio || 1; +let drawing = false; +let currentStroke = []; +let strokes = []; +let redoStack = []; + +let draggingHud = false; +let dragOffsetX = 0; +let dragOffsetY = 0; +let dragPointerId = null; + +let uiTimer = 0; +let hoverTimer = 0; + +let togglePressTimer = 0; +let togglePressActive = false; +let togglePressMoved = false; +let toggleDragStartedFromButton = false; +let toggleSuppressClick = false; +let togglePointerId = null; +let toggleStartX = 0; +let toggleStartY = 0; + +let pointerInsideHud = false; +let collapseLockUntil = 0; + +let lastDotX = 0; +let lastDotY = 0; +let dotRAF = 0; +let canvasScale = 1; +let dotStyleRAF = 0; +let colorPickerActive = false; + +const cursorIcons = cycleCursor ? cycleCursor.querySelectorAll(".cursor-ic") : []; + +function clamp(v, min, max) { + return Math.max(min, Math.min(max, v)); +} + +function clearTimer(t) { + return t && clearTimeout(t), 0; +} + +function now() { + return performance.now(); +} + +function lockCollapse(ms = COLLAPSE_GRACE_MS) { + collapseLockUntil = now() + ms; +} + +function syncRange(el) { + const min = Number(el.min || 0); + const max = Number(el.max || 100); + const val = Number(el.value); + const pct = ((val - min) / (max - min)) * 100; + el.style.setProperty("--pct", `${pct}%`); +} + +function isDark() { + return document.body.dataset.theme === "dark"; +} + +function getCursorMode() { + return document.body.dataset.cursor || localStorage.getItem(CURSOR_KEY) || "crosshair"; +} + +function updateCursorIcons() { + if (!cycleCursor || !cursorIcons.length) return; + const mode = getCursorMode(); + cursorIcons.forEach(i => i.classList.remove("active")); + const active = cycleCursor.querySelector(`.cursor-${mode}`); + if (active) active.classList.add("active"); +} + +function applyCursorDotVisibility() { + if (!cursorDot) return; + cursorDot.style.opacity = document.body.dataset.cursor === "dot" && !colorPickerActive ? "1" : "0"; +} + +function setCursorMode(mode) { + document.body.dataset.cursor = mode; + localStorage.setItem(CURSOR_KEY, mode); + updateCursorIcons(); + applyCursorDotVisibility(); + syncDotStyle(); +} + +function cycleCursorMode() { + const current = getCursorMode(); + const idx = cursorOrder.indexOf(current); + setCursorMode(cursorOrder[(idx + 1) % cursorOrder.length]); +} + +function measureCanvasScale() { + const rect = canvas.getBoundingClientRect(); + canvasScale = rect.width ? canvas.width / (rect.width * dpr) : 1; +} + +function updateCursorDotStyle() { + if (!cursorDot) return; + measureCanvasScale(); + const s = Number(size.value) || 10; + const dotSize = Math.max(6, Math.min(160, s / canvasScale)); + document.documentElement.style.setProperty("--cursor-dot-size", `${dotSize}px`); + const dotColor = eraser.checked + ? (isDark() ? "rgba(255,255,255,.32)" : "rgba(0,0,0,.22)") + : (colorA.value || strokeColor.value || "#000"); + document.documentElement.style.setProperty("--cursor-dot-color", dotColor); +} + +function syncDotStyle() { + if (dotStyleRAF) return; + dotStyleRAF = requestAnimationFrame(() => { + dotStyleRAF = 0; + updateCursorDotStyle(); + applyCursorDotVisibility(); + updateBrushPreview(); + }); +} + +function setDotPos() { + dotRAF = 0; + if (cursorDot) { + cursorDot.style.transform = `translate3d(${lastDotX}px,${lastDotY}px,0)`; + } +} + +function queueDotPos(x, y) { + lastDotX = x; + lastDotY = y; + if (!dotRAF) dotRAF = requestAnimationFrame(setDotPos); +} + +function saveSettings() { + localStorage.setItem(SETTINGS_KEY, JSON.stringify({ + size: size.value, + thinning: thinning.value, + smoothing: smoothing.value, + streamline: streamline.value, + pressureMode: pressureMode.value, + eraser: eraser.checked, + strokeColor: strokeColor.value, + colorA: colorA.value, + colorB: colorB.value, + gradientMix: gradientMix.value, + gradientMode: gradientMode.value, + guidesOn: guidesOn.checked, + guidesMode: guidesMode.value, + guidesSpacing: guidesSpacing.value, + guidesOpacity: guidesOpacity.value, + guidesSnap: guidesSnap.checked + })); +} + +function restoreSettings() { + try { + const s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "null"); + if (!s) return; + + if (s.size != null) { size.value = s.size; sizeRange.value = s.size; } + if (s.thinning != null) { thinning.value = s.thinning; thinningRange.value = s.thinning; } + if (s.smoothing != null) { smoothing.value = s.smoothing; smoothingRange.value = s.smoothing; } + if (s.streamline != null) { streamline.value = s.streamline; streamlineRange.value = s.streamline; } + if (s.pressureMode != null) pressureMode.value = s.pressureMode; + if (s.eraser != null) eraser.checked = !!s.eraser; + if (s.strokeColor) strokeColor.value = s.strokeColor; + if (s.colorA) colorA.value = s.colorA; + if (s.colorB) colorB.value = s.colorB; + if (s.gradientMix != null) { gradientMix.value = s.gradientMix; gradientMixRange.value = s.gradientMix; } + if (s.gradientMode) gradientMode.value = s.gradientMode; + if (s.guidesOn != null) guidesOn.checked = !!s.guidesOn; + if (s.guidesMode) guidesMode.value = s.guidesMode; + if (s.guidesSpacing != null) { guidesSpacing.value = s.guidesSpacing; guidesSpacingRange.value = s.guidesSpacing; } + if (s.guidesOpacity != null) { guidesOpacity.value = s.guidesOpacity; guidesOpacityRange.value = s.guidesOpacity; } + if (s.guidesSnap != null) guidesSnap.checked = !!s.guidesSnap; + } catch {} +} + +function bindPair(rangeEl, numberEl) { + const sync = src => { + if (rangeEl.value !== src.value) rangeEl.value = src.value; + if (numberEl.value !== src.value) numberEl.value = src.value; + syncRange(rangeEl); + syncRange(numberEl); + saveSettings(); + syncDotStyle(); + }; + + rangeEl.addEventListener("input", () => { + sync(rangeEl); + redraw(); + }); + numberEl.addEventListener("input", () => { + sync(numberEl); + redraw(); + }); + + syncRange(rangeEl); + syncRange(numberEl); +} + +function setTheme(theme) { + document.body.dataset.theme = theme === "dark" ? "dark" : "light"; + localStorage.setItem(THEME_KEY, document.body.dataset.theme); + redraw(); + syncDotStyle(); + updateBrushPreview(); +} + +function toggleThemeNow() { + setTheme(isDark() ? "light" : "dark"); +} + +function resizeCanvas() { + const rect = canvas.getBoundingClientRect(); + dpr = window.devicePixelRatio || 1; + canvas.width = Math.round(rect.width * dpr); + canvas.height = Math.round(rect.height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + measureCanvasScale(); + syncDotStyle(); + redraw(); +} + +function getPoint(ev) { + const rect = canvas.getBoundingClientRect(); + return { + x: ev.clientX - rect.left, + y: ev.clientY - rect.top, + pressure: ev.pressure && ev.pressure > 0 ? ev.pressure : 0.5 + }; +} + +function simulatedPressure(points, index) { + if (index === 0) return 0.5; + const start = Math.max(0, index - 3); + let total = 0; + let count = 0; + for (let i = start + 1; i <= index; i++) { + const p1 = points[i - 1]; + const p2 = points[i]; + if (p1 && p2) { + total += Math.hypot(p2.x - p1.x, p2.y - p1.y); + count++; + } + } + const avg = total / Math.max(1, count); + const pressure = 1 - Math.min(1, avg / 30); + return Math.max(0.2, Math.min(1, Math.pow(pressure, 1.25))); +} + +function hexToRgb(hex) { + const s = (hex || "#000000").replace("#", ""); + const full = s.length === 3 ? s.split("").map(ch => ch + ch).join("") : s; + const n = parseInt(full, 16); + return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 }; +} + +function rgbToHex(r, g, b) { + return "#" + [r, g, b].map(v => v.toString(16).padStart(2, "0")).join(""); +} + +function mixColors(a, b, t) { + const c1 = hexToRgb(a); + const c2 = hexToRgb(b); + const u = clamp(Number(t) || 0, 0, 1); + return rgbToHex( + Math.round(c1.r + (c2.r - c1.r) * u), + Math.round(c1.g + (c2.g - c1.g) * u), + Math.round(c1.b + (c2.b - c1.b) * u) + ); +} + +function brushState() { + return { + size: Number(size.value), + thinning: Number(thinning.value), + smoothing: Number(smoothing.value), + streamline: Number(streamline.value), + pressureMode: pressureMode.value, + colorA: colorA.value, + colorB: colorB.value, + gradientMix: Number(gradientMix.value), + gradientMode: gradientMode.value, + erase: eraser.checked + }; +} + +function drawStroke(stroke, preview = false, targetCtx = ctx) { + const points = stroke && stroke.points ? stroke.points : stroke; + const brush = stroke && stroke.brush ? stroke.brush : brushState(); + const filtered = points && points.length ? points.filter(p => p && Number.isFinite(p.x) && Number.isFinite(p.y)) : []; + if (!filtered.length) return; + + const outline = getStroke( + filtered.map((p, i) => [p.x, p.y, brush.pressureMode === "pointer" ? (p.pressure ?? 0.5) : simulatedPressure(filtered, i)]), + { + size: Number(brush.size), + thinning: Number(brush.thinning), + smoothing: Number(brush.smoothing), + streamline: Number(brush.streamline), + simulatePressure: false + } + ); + + if (!outline.length) return; + + targetCtx.save(); + targetCtx.globalCompositeOperation = brush.erase ? "destination-out" : "source-over"; + + if (brush.erase) { + targetCtx.fillStyle = "#000"; + targetCtx.beginPath(); + targetCtx.moveTo(outline[0][0], outline[0][1]); + outline.forEach(pt => targetCtx.lineTo(pt[0], pt[1])); + targetCtx.closePath(); + targetCtx.fill(); + targetCtx.restore(); + return; + } + + if (brush.gradientMode === "along" && outline.length > 1) { + for (let i = 0; i < outline.length - 1; i++) { + const t = i / (outline.length - 2 || 1); + const c = mixColors(brush.colorA, brush.colorB, clamp(brush.gradientMix + t * (1 - brush.gradientMix), 0, 1)); + targetCtx.fillStyle = c; + targetCtx.beginPath(); + targetCtx.moveTo(outline[i][0], outline[i][1]); + targetCtx.lineTo(outline[i + 1][0], outline[i + 1][1]); + const next = outline[i + 2] || outline[i + 1]; + const cur = outline[i + 1]; + targetCtx.lineTo(cur[0] + (next[0] - cur[0]) * 0.25, cur[1] + (next[1] - cur[1]) * 0.25); + targetCtx.closePath(); + targetCtx.fill(); + } + } else { + targetCtx.fillStyle = brush.gradientMode === "fixed" + ? mixColors(brush.colorA, brush.colorB, brush.gradientMix) + : mixColors(brush.colorA, brush.colorB, 0.5); + targetCtx.beginPath(); + targetCtx.moveTo(outline[0][0], outline[0][1]); + outline.forEach(pt => targetCtx.lineTo(pt[0], pt[1])); + targetCtx.closePath(); + targetCtx.fill(); + } + + targetCtx.restore(); +} + +function drawGuides() { + if (!guidesOn.checked) return; + const rect = canvas.getBoundingClientRect(); + const mode = guidesMode.value; + const opacity = clamp(Number(guidesOpacity.value) || 0, 0.01, 1); + const rgb = isDark() ? "255,255,255" : "0,0,0"; + + ctx.save(); + ctx.globalCompositeOperation = "source-over"; + ctx.lineWidth = 1; + ctx.strokeStyle = `rgba(${rgb},${opacity})`; + ctx.setLineDash([6, 6]); + + const spacing = Math.max(4, Number(guidesSpacing.value) || 80); + + if (mode === "crosshair") { + ctx.beginPath(); + ctx.moveTo(rect.width / 2, 0); + ctx.lineTo(rect.width / 2, rect.height); + ctx.moveTo(0, rect.height / 2); + ctx.lineTo(rect.width, rect.height / 2); + ctx.stroke(); + } else if (mode === "grid") { + for (let x = spacing; x < rect.width; x += spacing) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rect.height); + ctx.stroke(); + } + for (let y = spacing; y < rect.height; y += spacing) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rect.width, y); + ctx.stroke(); + } + } else { + for (let x = spacing; x < rect.width; x += spacing) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, rect.height); + ctx.stroke(); + } + for (let y = spacing; y < rect.height; y += spacing) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(rect.width, y); + ctx.stroke(); + } + } + + ctx.restore(); +} + +function redraw() { + const rect = canvas.getBoundingClientRect(); + const bg = isDark() ? "#111" : "#fff"; + ctx.clearRect(0, 0, rect.width, rect.height); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, rect.width, rect.height); + strokes.forEach(s => drawStroke(s, false, ctx)); + if (drawing && currentStroke.length) { + drawStroke({ points: currentStroke, brush: brushState() }, true, ctx); + } + drawGuides(); +} + +function collapseUi() { + if (!hud.classList.contains("compact")) { + hud.classList.add("compact"); + localStorage.setItem(UI_KEY, "compact"); + } +} + +function expandUi() { + if (hud.classList.contains("compact")) { + hud.classList.remove("compact"); + localStorage.setItem(UI_KEY, "expanded"); + } +} + +function hudHasFocus() { + const active = document.activeElement; + return !!active && hud.contains(active); +} + +function canAutoHide() { + return !draggingHud && + !drawing && + !pointerInsideHud && + !hud.matches(":hover") && + !hudHoverGap.matches(":hover") && + !hudHasFocus() && + now() > collapseLockUntil; +} + +function scheduleAutoCollapse(ms = AUTO_COLLAPSE_DELAY) { + uiTimer = clearTimer(uiTimer); + uiTimer = setTimeout(() => { + if (canAutoHide()) collapseUi(); + }, ms); +} + +function restoreUiState() { + if (localStorage.getItem(UI_KEY) === "compact") collapseUi(); + else expandUi(); +} + +function restoreHudPosition() { + try { + const pos = JSON.parse(localStorage.getItem(HUD_POS_KEY) || "null"); + if (pos) { + hud.style.left = `${pos.x}px`; + hud.style.top = `${pos.y}px`; + } + } catch {} +} + +function saveHudPosition() { + const rect = hud.getBoundingClientRect(); + localStorage.setItem(HUD_POS_KEY, JSON.stringify({ x: rect.left, y: rect.top })); +} + +function clampHudToViewport() { + const rect = hud.getBoundingClientRect(); + const w = window.innerWidth; + const h = window.innerHeight; + hud.style.left = `${clamp(rect.left, VIEWPORT_PAD, w - rect.width - VIEWPORT_PAD)}px`; + hud.style.top = `${clamp(rect.top, VIEWPORT_PAD, h - rect.height - VIEWPORT_PAD)}px`; +} + +function snapToCornerIfNear() { + const rect = hud.getBoundingClientRect(); + const w = window.innerWidth; + const h = window.innerHeight; + const nearLeft = rect.left < SNAP_PAD; + const nearTop = rect.top < SNAP_PAD; + const nearRight = w - rect.right < SNAP_PAD; + const nearBottom = h - rect.bottom < SNAP_PAD; + if (!(nearLeft || nearTop || nearRight || nearBottom)) return false; + + const x = nearLeft ? VIEWPORT_PAD : nearRight ? Math.max(VIEWPORT_PAD, w - rect.width - VIEWPORT_PAD) : rect.left; + const y = nearTop ? VIEWPORT_PAD : nearBottom ? Math.max(VIEWPORT_PAD, h - rect.height - VIEWPORT_PAD) : rect.top; + hud.style.left = `${x}px`; + hud.style.top = `${y}px`; + collapseUi(); + saveHudPosition(); + return true; +} + +function commitStroke() { + if (!currentStroke.length) return; + strokes.push({ points: currentStroke.slice(), brush: brushState() }); + currentStroke = []; + redoStack = []; + redraw(); + collapseUi(); + scheduleAutoCollapse(900); +} + +function stopDrawing() { + if (drawing) { + drawing = false; + commitStroke(); + } +} + +function pointerNearHandle(x, y, pad = HOVER_PAD) { + const rect = hudDragHandle.getBoundingClientRect(); + return x >= rect.left - pad && x <= rect.right + pad && y >= rect.top - pad && y <= rect.bottom + pad; +} + +function hoverExpandIfNeeded(x, y) { + if (!hud.classList.contains("compact")) return; + hoverTimer = clearTimer(hoverTimer); + if (pointerNearHandle(x, y)) { + hoverTimer = setTimeout(() => { + if (!draggingHud && !drawing && hud.classList.contains("compact")) expandUi(); + }, EXPAND_DELAY); + } +} + +function hoverCollapseIfNeeded() { + if (hud.classList.contains("compact")) return; + hoverTimer = clearTimer(hoverTimer); + hoverTimer = setTimeout(() => { + if (canAutoHide()) collapseUi(); + }, COLLAPSE_DELAY); +} + +function savePNG() { + const rect = canvas.getBoundingClientRect(); + const out = document.createElement("canvas"); + out.width = Math.round(rect.width * dpr); + out.height = Math.round(rect.height * dpr); + const octx = out.getContext("2d"); + const bg = isDark() ? "#111" : "#fff"; + octx.setTransform(dpr, 0, 0, dpr, 0, 0); + octx.fillStyle = bg; + octx.fillRect(0, 0, rect.width, rect.height); + referenceManager?.drawToExport(octx); + strokes.forEach(s => drawStroke(s, false, octx)); + const a = document.createElement("a"); + a.download = "draw.png"; + a.href = out.toDataURL("image/png"); + a.click(); +} + +function beginHudDrag(x, y, pointerId = null) { + draggingHud = true; + dragPointerId = pointerId; + hud.classList.add("dragging"); + const rect = hud.getBoundingClientRect(); + dragOffsetX = x - rect.left; + dragOffsetY = y - rect.top; + uiTimer = clearTimer(uiTimer); + hoverTimer = clearTimer(hoverTimer); + lockCollapse(); +} + +function moveHudDrag(x, y) { + if (!draggingHud) return; + hud.style.left = `${Math.max(0, x - dragOffsetX)}px`; + hud.style.top = `${Math.max(0, y - dragOffsetY)}px`; + if (x < SNAP_PAD || y < SNAP_PAD || x > window.innerWidth - SNAP_PAD || y > window.innerHeight - SNAP_PAD) { + snapToCornerIfNear(); + } +} + +function stopHudDrag() { + if (!draggingHud) return; + draggingHud = false; + dragPointerId = null; + hud.classList.remove("dragging"); + clampHudToViewport(); + if (!snapToCornerIfNear()) saveHudPosition(); + lockCollapse(); + scheduleAutoCollapse(900); +} + +function clearToggleLongPress() { + togglePressTimer = clearTimer(togglePressTimer); + togglePressActive = false; + togglePointerId = null; +} + +function resetToggleSuppressionSoon() { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + toggleSuppressClick = false; + toggleDragStartedFromButton = false; + }); + }); +} + +function startToggleLongPress(ev) { + togglePressActive = true; + togglePressMoved = false; + toggleSuppressClick = false; + toggleDragStartedFromButton = false; + togglePointerId = ev.pointerId; + toggleStartX = ev.clientX; + toggleStartY = ev.clientY; + lockCollapse(); + togglePressTimer = clearTimer(togglePressTimer); + togglePressTimer = setTimeout(() => { + if (togglePressActive && !togglePressMoved && !draggingHud) { + toggleDragStartedFromButton = true; + toggleSuppressClick = true; + expandUi(); + beginHudDrag(ev.clientX, ev.clientY, ev.pointerId); + } + }, LONG_PRESS_DELAY); +} + +function keepHudOpen() { + pointerInsideHud = true; + lockCollapse(); + uiTimer = clearTimer(uiTimer); + expandUi(); +} + +function handleGlobalPointerMove(ev) { + if (document.body.dataset.cursor === "dot") queueDotPos(ev.clientX, ev.clientY); + + if (draggingHud) { + if (dragPointerId == null || ev.pointerId === dragPointerId) moveHudDrag(ev.clientX, ev.clientY); + } else if (hud.classList.contains("compact")) { + hoverExpandIfNeeded(ev.clientX, ev.clientY); + } else if (hud.matches(":hover") || hudHoverGap.matches(":hover") || hudHasFocus()) { + // keep open + } else { + hoverCollapseIfNeeded(); + } +} + +function handleGlobalPointerEnd(ev) { + if (togglePressActive && togglePointerId === ev.pointerId) clearToggleLongPress(); + if (draggingHud && (dragPointerId == null || dragPointerId === ev.pointerId)) { + stopHudDrag(); + if (toggleDragStartedFromButton) resetToggleSuppressionSoon(); + } +} + +function setPanel(panel, toggle, open, key) { + if (!panel || !toggle) return; + panel.classList.toggle("open", open); + toggle.textContent = open ? "hide" : "show"; + localStorage.setItem(key, open ? "open" : "closed"); +} + + +/* reference manager */ +const referenceManager = (() => { + const refLayer = document.getElementById("refLayer"); + const refUpload = document.getElementById("refUpload"); + const addRefBtn = document.getElementById("addRef"); + if (!refLayer) return null; + + const state = { + refs: [], + activeId: null, + seq: 1, + menu: null, + ui: {} + }; + + function getCanvasCenter() { + const r = canvas.getBoundingClientRect(); + return { x: r.width / 2, y: r.height / 2 }; + } + + function normalizeZ() { + state.refs.sort((a, b) => a.z - b.z); + state.refs.forEach((r, i) => { + r.z = i; + r.item.style.zIndex = String(i); + }); + } + + function syncRefDom(ref) { + ref.item.style.left = `${ref.x - ref.w / 2}px`; + ref.item.style.top = `${ref.y - ref.h / 2}px`; + ref.item.style.width = `${ref.w}px`; + ref.item.style.height = `${ref.h}px`; + ref.item.style.opacity = ref.opacity; + ref.item.style.zIndex = String(ref.z); + const sx = ref.flipX ? -1 : 1; + const sy = ref.flipY ? -1 : 1; + ref.img.style.transform = `rotate(${ref.rotation}deg) scale(${sx}, ${sy})`; + ref.img.style.transformOrigin = "center center"; + } + + function clearFadeTimer(ref) { + if (ref?._fadeTimer) { + clearTimeout(ref._fadeTimer); + ref._fadeTimer = 0; + } + } + + function flashSelected(ref) { + if (!ref?.item) return; + ref.item.classList.remove("ref-fade"); + ref.item.classList.add("ref-selected"); + } + + function scheduleFade(ref) { + if (!ref?.item) return; + clearFadeTimer(ref); + ref._fadeTimer = setTimeout(() => { + if (state.activeId === ref.id) return; + ref.item.classList.add("ref-fade"); + ref.item.classList.remove("ref-selected"); + }, 3000); + } + + function getActiveRef() { + return state.refs.find(r => r.id === state.activeId) || null; + } + + function syncMenu(ref) { + if (!state.menu || !ref) return; + state.ui.opacity.value = ref.opacity; + state.ui.rotate.value = ref.rotation; + state.ui.flipX.textContent = ref.flipX ? "flip x: on" : "flip x"; + state.ui.flipY.textContent = ref.flipY ? "flip y: on" : "flip y"; + } + + function setActive(ref) { + const prev = getActiveRef(); + state.activeId = ref?.id ?? null; + + state.refs.forEach(r => { + if (r.id === state.activeId) { + clearFadeTimer(r); + r.item.classList.remove("ref-fade"); + r.item.classList.add("ref-selected"); + } else if (r.item.classList.contains("ref-selected")) { + scheduleFade(r); + } + }); + + if (prev && prev.id !== state.activeId) scheduleFade(prev); + if (ref) syncMenu(ref); + } + + function showMenu(ref, x, y) { + if (!state.menu) return; + setActive(ref); + state.menu.style.display = "block"; + state.menu.style.left = `${x}px`; + state.menu.style.top = `${y}px`; + syncMenu(ref); + } + + function hideMenu() { + if (state.menu) state.menu.style.display = "none"; + } + + function bringToFront(ref) { + ref.z = Math.max(-1, ...state.refs.map(r => r.z)) + 1; + normalizeZ(); + syncRefDom(ref); + } + + function sendToBack(ref) { + ref.z = Math.min(1e9, ...state.refs.map(r => r.z)) - 1; + normalizeZ(); + syncRefDom(ref); + } + + function moveUp(ref) { + const sorted = [...state.refs].sort((a, b) => a.z - b.z); + const idx = sorted.findIndex(r => r.id === ref.id); + if (idx >= 0 && idx < sorted.length - 1) { + [sorted[idx].z, sorted[idx + 1].z] = [sorted[idx + 1].z, sorted[idx].z]; + normalizeZ(); + } + } + + function moveDown(ref) { + const sorted = [...state.refs].sort((a, b) => a.z - b.z); + const idx = sorted.findIndex(r => r.id === ref.id); + if (idx > 0) { + [sorted[idx].z, sorted[idx - 1].z] = [sorted[idx - 1].z, sorted[idx].z]; + normalizeZ(); + } + } + + function createReference({ src, x, y, w, h, opacity = 1, rotation = 0, flipX = false, flipY = false, z = state.refs.length }) { + const id = state.seq++; + const item = document.createElement("div"); + item.className = "ref-item"; + const img = document.createElement("img"); + img.src = src; + img.alt = "reference"; + const handle = document.createElement("div"); + handle.className = "ref-handle"; + item.appendChild(img); + item.appendChild(handle); + refLayer.appendChild(item); + + const ref = { id, src, x, y, w, h, opacity, rotation, flipX, flipY, z, item, img, handle, _fadeTimer: 0 }; + state.refs.push(ref); + normalizeZ(); + syncRefDom(ref); + bindInteractions(ref); + flashSelected(ref); + return ref; + } + + function bindInteractions(ref) { + let drag = null; + let resize = null; + + const activate = () => { + setActive(ref); + bringToFront(ref); + }; + + ref.item.addEventListener("pointerdown", (e) => { + if (e.target === ref.handle) return; + activate(); + ref.item.classList.add("active-dragging"); + ref.item.setPointerCapture(e.pointerId); + drag = { id: e.pointerId, startX: e.clientX, startY: e.clientY, x: ref.x, y: ref.y }; + }); + + ref.handle.addEventListener("pointerdown", (e) => { + e.stopPropagation(); + activate(); + ref.item.setPointerCapture(e.pointerId); + resize = { id: e.pointerId, startX: e.clientX, startY: e.clientY, w: ref.w, h: ref.h, ratio: ref.h / ref.w }; + }); + + ref.item.addEventListener("contextmenu", (e) => { + e.preventDefault(); + showMenu(ref, e.clientX, e.clientY); + }); + + ref.item.addEventListener("wheel", (e) => { + if (!e.shiftKey) return; + e.preventDefault(); + ref.rotation += Math.sign(e.deltaY) * 5; + syncRefDom(ref); + syncMenu(ref); + }, { passive: false }); + + window.addEventListener("pointermove", (e) => { + if (drag && e.pointerId === drag.id) { + ref.x = drag.x + (e.clientX - drag.startX); + ref.y = drag.y + (e.clientY - drag.startY); + syncRefDom(ref); + } + if (resize && e.pointerId === resize.id) { + const dw = e.clientX - resize.startX; + ref.w = Math.max(40, resize.w + dw); + ref.h = ref.w * resize.ratio; + syncRefDom(ref); + } + }); + + window.addEventListener("pointerup", (e) => { + if (drag && e.pointerId === drag.id) { + drag = null; + ref.item.classList.remove("active-dragging"); + } + if (resize && e.pointerId === resize.id) resize = null; + }); + + window.addEventListener("pointercancel", (e) => { + if (drag && e.pointerId === drag.id) { + drag = null; + ref.item.classList.remove("active-dragging"); + } + if (resize && e.pointerId === resize.id) resize = null; + }); + } + + function removeRef(ref) { + clearFadeTimer(ref); + ref.item.remove(); + state.refs = state.refs.filter(r => r.id !== ref.id); + if (state.activeId === ref.id) state.activeId = null; + normalizeZ(); + hideMenu(); + } + + function duplicateRef(ref) { + const clone = createReference({ + src: ref.src, + x: ref.x + 20, + y: ref.y + 20, + w: ref.w, + h: ref.h, + opacity: ref.opacity, + rotation: ref.rotation, + flipX: ref.flipX, + flipY: ref.flipY, + z: ref.z + 1 + }); + setActive(clone); + bringToFront(clone); + } + + async function addFiles(files) { + const list = [...files].filter(f => f.type && f.type.startsWith("image/")); + const center = getCanvasCenter(); + for (const file of list) { + const src = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + const maxW = Math.min(canvas.getBoundingClientRect().width * 0.45, img.naturalWidth || 300); + const ratio = (img.naturalHeight || 200) / (img.naturalWidth || 300); + const w = Math.max(120, maxW); + const h = Math.max(80, w * ratio); + createReference({ src, x: center.x, y: center.y, w, h, opacity: 1, rotation: 0, flipX: false, flipY: false }); + }; + img.src = src; + } + } + + function drawToExport(exportCtx) { + [...state.refs].sort((a, b) => a.z - b.z).forEach(ref => { + if (!ref.img.complete) return; + exportCtx.save(); + exportCtx.globalAlpha = ref.opacity; + exportCtx.translate(ref.x, ref.y); + exportCtx.rotate((ref.rotation * Math.PI) / 180); + exportCtx.scale(ref.flipX ? -1 : 1, ref.flipY ? -1 : 1); + exportCtx.drawImage(ref.img, -ref.w / 2, -ref.h / 2, ref.w, ref.h); + exportCtx.restore(); + }); + } + + function init() { + if (addRefBtn && refUpload) { + addRefBtn.addEventListener("click", () => refUpload.click()); + refUpload.addEventListener("change", async e => { + if (e.target.files?.length) await addFiles(e.target.files); + refUpload.value = ""; + }); + } + + window.addEventListener("dragover", e => e.preventDefault()); + window.addEventListener("drop", async e => { + e.preventDefault(); + if (e.dataTransfer?.files?.length) await addFiles(e.dataTransfer.files); + }); + + window.addEventListener("keydown", e => { + if (e.key === "Escape") hideMenu(); + }); + } + + init(); + + return { + createReference, + addFiles, + drawToExport, + get refs() { return state.refs; }, + get active() { return getActiveRef(); }, + setActive, + hideMenu, + bringToFront, + sendToBack, + moveUp, + moveDown + }; +})(); + +/* init UI toggles */ +function setPanelState(panel, toggle, open, key) { + if (!panel || !toggle) return; + panel.classList.toggle("open", open); + toggle.textContent = open ? "hide" : "show"; + localStorage.setItem(key, open ? "open" : "closed"); +} + +gradientToggle?.addEventListener("click", () => { + setPanelState(gradientPanel, gradientToggle, !gradientPanel.classList.contains("open"), "pfh-gradient-panel"); +}); + +guidesToggle?.addEventListener("click", () => { + setPanelState(guidesPanel, guidesToggle, !guidesPanel.classList.contains("open"), "pfh-guides-panel"); +}); + +setPanelState(gradientPanel, gradientToggle, localStorage.getItem("pfh-gradient-panel") === "open", "pfh-gradient-panel"); +setPanelState(guidesPanel, guidesToggle, localStorage.getItem("pfh-guides-panel") === "open", "pfh-guides-panel"); + +/* brush preview */ +function updateBrushPreview() { + if (!brushPreview) return; + const color = eraser.checked + ? (isDark() ? "rgba(255,255,255,.32)" : "rgba(0,0,0,.22)") + : (strokeColor?.value || "#000000"); + brushPreview.style.background = color; + brushPreview.style.height = `${Math.max(2, Math.min(16, Number(size?.value || sizeRange?.value || 12) / 2))}px`; +} + +/* bootstrap */ +bindPair(sizeRange, size); +bindPair(thinningRange, thinning); +bindPair(smoothingRange, smoothing); +bindPair(streamlineRange, streamline); +bindPair(gradientMixRange, gradientMix); + +restoreSettings(); +document.body.dataset.theme = localStorage.getItem(THEME_KEY) || "dark"; +setCursorMode(localStorage.getItem(CURSOR_KEY) || "crosshair"); +restoreHudPosition(); +restoreUiState(); +resizeCanvas(); +redraw(); +syncDotStyle(); +updateCursorIcons(); +updateBrushPreview(); + +/* canvas draw events */ +canvas.addEventListener("pointerdown", ev => { + drawing = true; + currentStroke = []; + const p = getPoint(ev); + if (p) currentStroke.push(p); + canvas.setPointerCapture(ev.pointerId); + redraw(); + uiTimer = clearTimer(uiTimer); + hoverTimer = clearTimer(hoverTimer); +}); +canvas.addEventListener("pointermove", ev => { + if (!drawing) return; + const p = getPoint(ev); + if (p) currentStroke.push(p); + redraw(); + scheduleAutoCollapse(1600); +}); +canvas.addEventListener("pointerup", stopDrawing); +canvas.addEventListener("pointercancel", stopDrawing); +canvas.addEventListener("pointerleave", stopDrawing); + +/* footer actions */ +clearBtn?.addEventListener("click", () => { + strokes = []; + redoStack = []; + currentStroke = []; + redraw(); + collapseUi(); + scheduleAutoCollapse(); +}); +undoBtn?.addEventListener("click", () => { + if (strokes.length) { + redoStack.push(strokes.pop()); + redraw(); + scheduleAutoCollapse(); + } +}); +redoBtn?.addEventListener("click", () => { + if (redoStack.length) { + strokes.push(redoStack.pop()); + redraw(); + scheduleAutoCollapse(); + } +}); +saveBtn?.addEventListener("click", savePNG); + +/* reset guides */ +resetGuides?.addEventListener("click", () => { + guidesOn.checked = true; + guidesMode.value = "crosshair"; + guidesSpacing.value = 80; + guidesSpacingRange.value = 80; + guidesOpacity.value = 0.18; + guidesOpacityRange.value = 0.18; + guidesSnap.checked = false; + saveSettings(); + redraw(); +}); + +/* toggle buttons */ +toggleTheme?.addEventListener("click", ev => { + if (toggleSuppressClick || toggleDragStartedFromButton || draggingHud) { + ev.preventDefault(); + ev.stopPropagation(); + return; + } + toggleThemeNow(); +}); +cycleCursor?.addEventListener("click", ev => { + ev.preventDefault(); + cycleCursorMode(); +}); + +/* brush color visibility */ +size?.addEventListener("input", syncDotStyle, { passive: true }); +size?.addEventListener("change", syncDotStyle); +sizeRange?.addEventListener("input", syncDotStyle, { passive: true }); +sizeRange?.addEventListener("change", syncDotStyle); +eraser?.addEventListener("input", syncDotStyle, { passive: true }); +eraser?.addEventListener("change", syncDotStyle); + +strokeColor?.addEventListener("pointerdown", () => { colorPickerActive = true; applyCursorDotVisibility(); }, { passive: true }); +strokeColor?.addEventListener("focus", () => { colorPickerActive = true; applyCursorDotVisibility(); }); +strokeColor?.addEventListener("input", syncDotStyle, { passive: true }); +strokeColor?.addEventListener("change", () => { syncDotStyle(); colorPickerActive = false; applyCursorDotVisibility(); }); +strokeColor?.addEventListener("blur", () => { syncDotStyle(); colorPickerActive = false; applyCursorDotVisibility(); }); +window.addEventListener("pointerup", () => { + if (colorPickerActive) { + syncDotStyle(); + colorPickerActive = false; + applyCursorDotVisibility(); + } +}, { passive: true }); + +/* global settings redraw hooks */ +[size, thinning, smoothing, streamline, pressureMode, eraser, strokeColor, colorA, colorB, gradientMix, gradientMode, guidesOn, guidesMode, guidesSpacing, guidesOpacity, guidesSnap].forEach(el => { + if (!el) return; + el.addEventListener("input", () => { + redraw(); + saveSettings(); + scheduleAutoCollapse(); + }); + el.addEventListener("change", () => { + redraw(); + saveSettings(); + scheduleAutoCollapse(); + }); +}); + +/* hud interactions */ +hud?.addEventListener("pointerdown", keepHudOpen); +hud?.addEventListener("focusin", keepHudOpen); +hud?.addEventListener("input", keepHudOpen); +hud?.addEventListener("change", keepHudOpen); +hud?.addEventListener("focusout", () => { lockCollapse(); scheduleAutoCollapse(); }); +hud?.addEventListener("pointerenter", () => { pointerInsideHud = true; uiTimer = clearTimer(uiTimer); lockCollapse(); }); +hud?.addEventListener("pointerleave", () => { pointerInsideHud = false; lockCollapse(); scheduleAutoCollapse(220); }); +hudTop?.addEventListener("pointerleave", () => lockCollapse()); +toggleTheme?.addEventListener("pointerleave", () => lockCollapse()); +hudHoverGap?.addEventListener("pointerenter", () => { pointerInsideHud = true; lockCollapse(); }); +hudHoverGap?.addEventListener("pointerleave", () => { pointerInsideHud = false; lockCollapse(); }); + +window.addEventListener("pointerleave", applyCursorDotVisibility); +window.addEventListener("pointerenter", applyCursorDotVisibility); + +/* keys */ +document.addEventListener("keydown", ev => { + if (!(ev.ctrlKey || ev.metaKey || ev.altKey) && ev.key.toLowerCase() === "c") { + ev.preventDefault(); + cycleCursorMode(); + return; + } + + const mod = ev.ctrlKey || ev.metaKey; + if (mod && ev.key.toLowerCase() === "s") { + ev.preventDefault(); + savePNG(); + return; + } + if (mod && ev.key.toLowerCase() === "z" && !ev.shiftKey) { + ev.preventDefault(); + undoBtn?.click(); + return; + } + if (mod && (ev.key.toLowerCase() === "y" || (ev.shiftKey && ev.key.toLowerCase() === "z"))) { + ev.preventDefault(); + redoBtn?.click(); + return; + } + if (ev.key === "Escape") { + if (hud.classList.contains("compact")) expandUi(); + else collapseUi(); + return; + } + + if (document.activeElement === hudDragHandle || document.activeElement === hud) { + const step = ev.shiftKey ? 24 : 12; + const rect = hud.getBoundingClientRect(); + let x = rect.left; + let y = rect.top; + let moved = false; + + if (ev.key === "ArrowLeft") { x -= step; moved = true; } + if (ev.key === "ArrowRight") { x += step; moved = true; } + if (ev.key === "ArrowUp") { y -= step; moved = true; } + if (ev.key === "ArrowDown") { y += step; moved = true; } + + if (moved) { + ev.preventDefault(); + hud.style.left = `${clamp(x, VIEWPORT_PAD, window.innerWidth - rect.width - VIEWPORT_PAD)}px`; + hud.style.top = `${clamp(y, VIEWPORT_PAD, window.innerHeight - rect.height - VIEWPORT_PAD)}px`; + saveHudPosition(); + lockCollapse(); + scheduleAutoCollapse(900); + } + + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + if (hud.classList.contains("compact")) expandUi(); + else collapseUi(); + } + } +}); + +/* hud drag wiring */ +hudTop?.addEventListener("pointerdown", ev => { + if (ev.button !== undefined && ev.button !== 0) return; + if (toggleTheme?.contains(ev.target)) return; + if (cycleCursor?.contains(ev.target)) return; + if (ev.target.closest(INTERACTIVE)) return; + beginHudDrag(ev.clientX, ev.clientY, ev.pointerId); +}); +hudDragHandle?.addEventListener("pointerdown", ev => { + if (ev.button !== undefined && ev.button !== 0) return; + if (toggleTheme?.contains(ev.target)) return; + if (cycleCursor?.contains(ev.target)) return; + beginHudDrag(ev.clientX, ev.clientY, ev.pointerId); + try { hudDragHandle.setPointerCapture(ev.pointerId); } catch {} +}); + +toggleTheme?.addEventListener("pointerdown", ev => { + if (ev.button !== undefined && ev.button !== 0) return; + startToggleLongPress(ev); +}); +toggleTheme?.addEventListener("pointermove", ev => { + if (draggingHud) { + if (dragPointerId === ev.pointerId) moveHudDrag(ev.clientX, ev.clientY); + return; + } + if (!togglePressActive || togglePointerId !== ev.pointerId) return; + const dx = ev.clientX - toggleStartX; + const dy = ev.clientY - toggleStartY; + if (Math.hypot(dx, dy) > LONG_PRESS_MOVE_TOLERANCE) { + togglePressMoved = true; + togglePressTimer = clearTimer(togglePressTimer); + } +}); +toggleTheme?.addEventListener("pointerup", ev => { + if (togglePointerId !== null && ev.pointerId === togglePointerId) { + clearToggleLongPress(); + if (toggleDragStartedFromButton) { + stopHudDrag(); + resetToggleSuppressionSoon(); + } + } +}); +toggleTheme?.addEventListener("pointercancel", ev => { + if (togglePointerId !== null && ev.pointerId === togglePointerId) { + clearToggleLongPress(); + if (toggleDragStartedFromButton || draggingHud) { + stopHudDrag(); + resetToggleSuppressionSoon(); + } + } +}); +toggleTheme?.addEventListener("contextmenu", ev => { + if (togglePressActive || draggingHud) ev.preventDefault(); +}); + +hudHoverGap?.addEventListener("pointerenter", () => { + if (!hud.classList.contains("compact")) return; + const rect = hudDragHandle.getBoundingClientRect(); + hoverExpandIfNeeded(rect.left + 1, rect.top + 1); +}); + +window.addEventListener("pointermove", handleGlobalPointerMove, { passive: true }); +window.addEventListener("pointerup", handleGlobalPointerEnd); +window.addEventListener("pointercancel", handleGlobalPointerEnd); +window.addEventListener("pointerdown", ev => { + if (hud.classList.contains("compact")) hoverExpandIfNeeded(ev.clientX, ev.clientY); + if (document.body.dataset.cursor === "dot") queueDotPos(ev.clientX, ev.clientY); +}, { passive: true }); + +window.addEventListener("resize", () => { + resizeCanvas(); + clampHudToViewport(); + syncDotStyle(); +}); +window.addEventListener("orientationchange", () => { + resizeCanvas(); + clampHudToViewport(); + syncDotStyle(); +}); +window.addEventListener("blur", () => { + clearToggleLongPress(); + if (draggingHud) stopHudDrag(); + pointerInsideHud = false; + lockCollapse(); + scheduleAutoCollapse(500); + applyCursorDotVisibility(); +}); + +setTimeout(() => { + if (!strokes.length) scheduleAutoCollapse(900); +}, 50); + +/* reference export hook exposed globally */ +window.referenceManager = referenceManager; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..490e07c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,938 @@ +{ + "name": "perfect-freehand-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "perfect-freehand-demo", + "version": "1.0.0", + "dependencies": { + "perfect-freehand": "^1.2.2" + }, + "devDependencies": { + "vite": "^5.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/perfect-freehand": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.3.tgz", + "integrity": "sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c56e0fb --- /dev/null +++ b/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + host: '0.0.0.0', + port: 80 + }, + preview: { + host: '0.0.0.0', + port: 80 + } +})