From 2eab8269790983ae82afab758c8cd9fe89233862 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Thu, 4 Jun 2026 18:20:50 +0900 Subject: [PATCH] ---- --- app.js | 80 ++++++++++++++++++++++++++++++++++++------------------ index.html | 5 ++++ styles.css | 22 ++++++++++++++- 3 files changed, 79 insertions(+), 28 deletions(-) diff --git a/app.js b/app.js index f375fa4..002786b 100644 --- a/app.js +++ b/app.js @@ -58,37 +58,28 @@ const uid = () => Math.random().toString(36).slice(2, 9) + Date.now().toString(36).slice(-5); const mod = (n, m) => ((n % m) + m) % m; const lerp = (a, b, t) => a + (b - a) * t; - - function clampDimension(value, fallback = 8) { return PixelCodec.clampDimension ? PixelCodec.clampDimension(value, fallback) : Math.max(1, Math.min(MAX_EDITOR_DIMENSION, Math.round(Number(value) || fallback))); } - function rectArea(width, height = width) { return Math.max(1, Math.floor(Number(width) || 1)) * Math.max(1, Math.floor(Number(height) || width || 1)); } - function lightBudgetForArea(width, height = width) { return Math.max(1, Math.ceil(rectArea(width, height) / 100)); } - function assetWidth(asset) { return PixelCodec.assetWidth ? PixelCodec.assetWidth(asset) : clampDimension(asset?.width ?? asset?.w ?? asset?.size, 16); } - function assetHeight(asset) { return PixelCodec.assetHeight ? PixelCodec.assetHeight(asset) : clampDimension(asset?.height ?? asset?.ht ?? asset?.size, assetWidth(asset)); } - function assetMaxSize(asset) { return PixelCodec.assetMaxSize ? PixelCodec.assetMaxSize(asset) : Math.max(assetWidth(asset), assetHeight(asset)); } - function clampInt(value, min, max, fallback = min) { const n = Math.round(Number(value)); return PixelCodec.clampInt ? PixelCodec.clampInt(value, min, max, fallback) : (Number.isFinite(n) ? clamp(n, min, max) : fallback); } - function buildPixelBlob(encodedPixels, width, height = width) { return PixelCodec.buildPixelBlob(encodedPixels, width, height, PALETTE, DEFAULT_SELECTED_COLOR_CODE); } @@ -283,8 +274,8 @@ staticKindWrap: $('staticKindWrap'), dynamicKindWrap: $('dynamicKindWrap'), roleHint: $('roleHint'), - sideSwitcher: $('sideSwitcher'), - editRight: $('editRight'), editLeft: $('editLeft'), + sideSwitcher: $('sideSwitcher'), frontSideSwitcher: $('frontSideSwitcher'), + editRight: $('editRight'), editLeft: $('editLeft'), frontRight: $('frontRight'), frontLeft: $('frontLeft'), paintColor: $('paintColor'), toolBrush: $('toolBrush'), toolErase: $('toolErase'), toolFill: $('toolFill'), toolPick: $('toolPick'), toolLine: $('toolLine'), toolRect: $('toolRect'), toolSelect: $('toolSelect'), undoPaint: $('undoPaint'), redoPaint: $('redoPaint'), toolLight: $('toolLight'), toolParticle: $('toolParticle'), toolDoor: $('toolDoor'), toolDepth: $('toolDepth'), depthHigh: $('depthHigh'), depthLow: $('depthLow'), toggleAdvanced: $('toggleAdvanced'), advancedHint: $('advancedHint'), advancedToolGroup: $('advancedToolGroup'), particleDirectionWrap: $('particleDirectionWrap'), particleDirection: $('particleDirection'), particleUseSelection: $('particleUseSelection'), particleClearRange: $('particleClearRange'), particleRangeStatus: $('particleRangeStatus'), clearPaint: $('clearPaint'), flipHorizontal: $('flipHorizontal'), flipVertical: $('flipVertical'), outlinePaint: $('outlinePaint'), clearSelection: $('clearSelection'), nudgeLeft: $('nudgeLeft'), nudgeRight: $('nudgeRight'), nudgeUp: $('nudgeUp'), nudgeDown: $('nudgeDown'), paintCanvas: $('paintCanvas'), editHint: $('editHint'), paletteGrid: $('paletteGrid'), @@ -332,6 +323,7 @@ let editorPixels = blankPixels(8); let editorLeftPixels = blankPixels(8); let editingSide = 'right'; + let frontSide = 'right'; let paintTool = 'brush'; let selectedColorCode = DEFAULT_SELECTED_COLOR_CODE; let advancedDraw = false; @@ -464,7 +456,8 @@ }); els.createAccount?.addEventListener('click', () => createLocalAccount(false)); - els.placeHereButton?.addEventListener('click', confirmPreviewPlacement); + els.placeHereButton?.addEventListener('pointerdown', (event) => { event.preventDefault(); event.stopPropagation(); confirmPreviewPlacement(); }); + els.placeHereButton?.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (placementPreview?.asset) confirmPreviewPlacement(); }); els.settingLights?.addEventListener('change', () => { visualSettings().enableLights = !!els.settingLights.checked; @@ -552,6 +545,8 @@ els.editRight.addEventListener('click', () => setEditingSide('right')); els.editLeft.addEventListener('click', () => setEditingSide('left')); + els.frontRight?.addEventListener('click', () => setFrontSide('right')); + els.frontLeft?.addEventListener('click', () => setFrontSide('left')); els.toolBrush.addEventListener('click', () => setPaintTool('brush')); els.toolErase.addEventListener('click', () => setPaintTool('erase')); @@ -734,6 +729,7 @@ els.panels.forEach((panel) => panel.classList.toggle('active', panel.id === `tab-${name}`)); if (name !== 'draw' && advancedDraw) setAdvancedDraw(false, { redraw: false }); else els.drawer?.classList.toggle('advancedTools', name === 'draw' && advancedDraw); + refreshCategoryUI(); if (keepScroll) restoreLibraryScrollState(scrollState); } @@ -805,7 +801,8 @@ toggleHidden(els.staticKindWrap, true); toggleHidden(els.dynamicKindWrap, true); - toggleHidden(els.sideSwitcher, isStatic); + const showDynamicDrawControls = activeTabName === 'draw' && !isStatic; + toggleHidden(els.sideSwitcher, true); toggleHidden(els.frontSideSwitcher, !showDynamicDrawControls); toggleHidden(els.staticSettingsPanel, !isStatic); toggleHidden(els.dynamicSettingsPanel, true); toggleHidden(els.advancedToolGroup, !advancedDraw); @@ -822,6 +819,7 @@ if (!isStatic && paintTool === 'door') setPaintTool('brush'); if (isStatic && role !== 'building' && paintTool === 'door') setPaintTool('brush'); if (isStatic) editingSide = 'right'; + if (isStatic) frontSide = 'right'; updateRoleHint(); updateSideButtons(); @@ -860,7 +858,7 @@ } else { const particleText = particlePixels.length ? ` / particles ${particlePixels.length} cell${particlePixels.length === 1 ? '' : 's'}` : ''; const sideText = editingSide === 'left' ? 'canvas marked Left; Save mirrors it into canonical Right' : 'canvas marked canonical Right'; - els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}x${editorHeight} canvas / ${sideText}${particleText}.`; + els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}x${editorHeight} canvas / ${sideText} / ${frontSide === 'left' ? 'front starts Left' : 'front starts Right'}${particleText}.`; } } @@ -909,14 +907,23 @@ drawEditor(); } + function setFrontSide(side) { + if (roleToCategory(currentRole()) !== 'dynamic') return; + frontSide = side === 'left' ? 'left' : 'right'; updateSideButtons(); + if (els.editHint) els.editHint.textContent = `Movable sprite will appear facing ${frontSide} when first placed.`; + spriteCache.clear(); drawEditor(); render(); + } + function updateSideButtons() { if (els.editRight) els.editRight.classList.toggle('active', editingSide === 'right'); if (els.editLeft) els.editLeft.classList.toggle('active', editingSide === 'left'); + if (els.frontRight) els.frontRight.classList.toggle('active', frontSide === 'right'); if (els.frontLeft) els.frontLeft.classList.toggle('active', frontSide === 'left'); updateSettingsSummary(); } function onWorldPointerDown(event) { event.preventDefault(); + if (isPlaceHereEvent(event)) { confirmPreviewPlacement(); return; } const downPos = getCanvasPoint(event); cursorScreen = { x: downPos.x, y: downPos.y, active: true }; if (event.button === 2) { @@ -970,6 +977,7 @@ } function onWorldPointerUp(event) { + if (isPlaceHereEvent(event)) { confirmPreviewPlacement(); return; } if (!pointer.down || pointer.id !== event.pointerId) return; els.canvas.classList.remove('dragging'); const total = Math.hypot(event.clientX - pointer.startX, event.clientY - pointer.startY); @@ -995,6 +1003,11 @@ pointer.dragging = false; } + function isPlaceHereEvent(event) { + if (mode !== 'place' || !placementPreview?.asset || !els.placeHereButton || els.placeHereButton.hidden) return false; + const rect = els.placeHereButton.getBoundingClientRect(); return event.clientX >= rect.left - 8 && event.clientX <= rect.right + 8 && event.clientY >= rect.top - 8 && event.clientY <= rect.bottom + 8; + } + function onWorldWheel(event) { event.preventDefault(); cameraFollowSelected = false; @@ -1684,7 +1697,7 @@ } if (placementPreview?.asset && placementPreview.x != null && placementPreview.y != null) { const asset = placementPreview.asset; - items.push({ kind: asset.category === 'dynamic' ? 'dynamic' : 'static', asset, x: placementPreview.x + .5, y: placementPreview.y + .5, source: { id: 'placement-preview', preview: true }, preview: true }); + items.push({ kind: asset.category === 'dynamic' ? 'dynamic' : 'static', asset, x: placementPreview.x + .5, y: placementPreview.y + .5, source: { id: 'placement-preview', preview: true, selectedAt: placementPreview.selectedAt || performance.now() }, preview: true }); } items.sort(drawOrderCompare); return items; @@ -1862,7 +1875,7 @@ const tile = world.get(x, y); if (!canPlace(asset, tile)) return; const savedAssetId = placementPreview?.savedAssetId || selectedAssetId || asset.id; - placementPreview = { asset, savedAssetId, x, y }; + placementPreview = { asset, savedAssetId, x, y, selectedAt: performance.now() }; clearWorldSelection(false); syncPlacementUi(); if (visualSettings().enableParticles) spawnEffects.push({ x: x + .5, y: y + .5, started: performance.now(), preview: true }); @@ -1885,6 +1898,7 @@ } placementPreview.x = x; placementPreview.y = y; + placementPreview.selectedAt = performance.now(); if (visualSettings().enableParticles) spawnEffects.push({ x: x + .5, y: y + .5, started: performance.now(), preview: true }); render(); toast(adjusted.moved ? 'Preview nudged away from the remix source. Right-click or DRAW to exit.' : 'Preview set. Right-click or DRAW to exit.'); @@ -3501,7 +3515,7 @@ } : null, meta: buildAssetMeta(category, subtype, aligned.depthPixels, aligned.lightPixels, selectedColorCode, aligned.door, savedWidth, aligned.particlePixels, aligned.rightPixels, savedHeight) }; - asset.contentHash = computeAssetContentHash(asset); + if (asset.meta && category === 'dynamic') asset.meta.frontSide = frontSide; asset.contentHash = computeAssetContentHash(asset); return { asset, pixelBlob, existing, aligned }; } @@ -3607,7 +3621,7 @@ faces: category === 'dynamic' ? { right: encodedRight, left: 'mirror' } : null, meta: buildAssetMeta(category, subtype, aligned.depthPixels, aligned.lightPixels, selectedColorCode, aligned.door, savedWidth, aligned.particlePixels, aligned.rightPixels, savedHeight) }; - return asset; + if (asset.meta && category === 'dynamic') asset.meta.frontSide = frontSide; return asset; } function checkCurrentEditorOnIsland() { @@ -3634,9 +3648,11 @@ toast('Click a tile first.'); return; } - const assetId = placementPreview.savedAssetId || placementPreview.asset.id; - if (!findAsset(assetId)) { - toast('Saved asset is missing. Back to canvas and save again.'); + let assetId = placementPreview.savedAssetId || placementPreview.asset.id; + if (!findAsset(assetId) && String(assetId).startsWith('preview:')) { + const saved = saveAssetFromEditor(); if (saved) assetId = saved.id; + } + if (!findAsset(assetId)) { toast('Saved asset is missing. Back to canvas and save again.'); return; } selectedAssetId = assetId; @@ -3656,7 +3672,7 @@ staticKind = 'nature'; dynamicKind = 'human'; editingSide = 'right'; - lightPixels = []; + frontSide = 'right'; lightPixels = []; particlePixels = []; particleConfig = { enabled: false, c: selectedColorCode, dir: 'up' }; depthPixels = blankPixels(8).map(() => 0); @@ -4292,7 +4308,7 @@ els.assetCategory.value = subtypeToRole(asset); staticKind = asset.category === 'static' ? asset.subtype : staticKind; dynamicKind = asset.category === 'dynamic' ? asset.subtype : dynamicKind; - editingSide = 'right'; + editingSide = 'right'; frontSide = asset.meta?.frontSide === 'left' ? 'left' : 'right'; const right = getAssetPixels(asset, 'right'); lightPixels = (asset.meta?.lightPixels || []).map((p) => ({ x: p.x, y: p.y, c: p.c || asset.meta?.lightColor || selectedColorCode })); particlePixels = (asset.meta?.particlePixels || []).map((p) => ({ x: p.x, y: p.y, c: p.c || selectedColorCode, dir: p.dir || 'up' })); @@ -4449,7 +4465,7 @@ const startY = Number.isFinite(Number(synced.y)) ? Number(synced.y) : (previous ? previous.y : spawn.y + .5); const targetX = Number.isFinite(Number(synced.targetX)) ? Number(synced.targetX) : (previous ? previous.targetX : startX); const targetY = Number.isFinite(Number(synced.targetY)) ? Number(synced.targetY) : (previous ? previous.targetY : startY); - const facing = Number.isFinite(Number(synced.facing)) && Number(synced.facing) !== 0 ? Math.sign(Number(synced.facing)) : (previous?.facing || 1); + const facing = Number.isFinite(Number(synced.facing)) && Number(synced.facing) !== 0 ? Math.sign(Number(synced.facing)) : (previous?.facing || (asset.meta?.frontSide === 'left' ? -1 : 1)); return { id: summon.id, assetId: summon.assetId, @@ -5125,8 +5141,8 @@ const runtime = item.source || {}; const seed = Number.isFinite(runtime.seed) ? runtime.seed : 0; const isPreview = Boolean(item.preview || runtime.preview); - const moveFacing = Math.abs(runtime.lastMoveX || 0) > 0.003 ? Math.sign(runtime.lastMoveX) : (runtime.facing || runtime.vx || 1); - side = isPreview ? 'right' : (moveFacing < 0 ? 'left' : 'right'); + const moveFacing = Math.abs(runtime.lastMoveX || 0) > 0.003 ? Math.sign(runtime.lastMoveX) : (runtime.facing || runtime.vx || (asset.meta?.frontSide === 'left' ? -1 : 1)); + side = isPreview ? (asset.meta?.frontSide === 'left' ? 'left' : 'right') : (moveFacing < 0 ? 'left' : 'right'); if (asset.subtype === 'human' || asset.subtype === 'animal') { const idle = isPreview || time < (runtime.idleUntil || 0); if (!idle) { @@ -5166,6 +5182,14 @@ } } + if (item.preview) { + const previewElapsed = time - (item.source?.selectedAt || item.source?.createdAt || time); + if (previewElapsed < 720) { + const hop = Math.sin(clamp(previewElapsed / 720, 0, 1) * Math.PI); + const settle = Math.exp(-Math.pow((previewElapsed - 470) / 120, 2)); bob += -hop * 12; stretchX += settle * 0.28; stretchY -= settle * 0.2; + } + } + const sprite = getSpriteCanvas(asset, side); const drawX = pos.x - sprite.width / 2; const anchorY = asset.category === 'static' ? 0 : TILE_H / 2; @@ -6712,6 +6736,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) : []; const normalized = buildAssetMeta(category, subtype, normalizedDepth, lightPixels, meta.lightColor || '#ffd86a', meta.door || null, width, legacyParticlePixels, sourcePixels, height); + if (category === 'dynamic') normalized.frontSide = meta.frontSide === 'left' ? 'left' : 'right'; return normalized; } @@ -7100,6 +7125,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { JSON.stringify(asset.meta?.lightPixels || []), JSON.stringify(asset.meta?.particlePixels || []), JSON.stringify(asset.meta?.particleConfig || null), + asset.meta?.frontSide || '', asset.meta?.door ? `${asset.meta.door.x},${asset.meta.door.y}` : '' ].join('|'); return `fnv1a:${fnv1a(payload)}`; @@ -7671,4 +7697,4 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { // Pixel Island default-art drawing code was moved out of app.js. window.PixelIslandDebug = { ...(window.PixelIslandDebug || {}), applySyncEvent }; bootstrap(); -})(); \ No newline at end of file +})(); diff --git a/index.html b/index.html index 3a710b5..4b78028 100644 --- a/index.html +++ b/index.html @@ -71,6 +71,11 @@ +
diff --git a/styles.css b/styles.css index 4ba565e..ff6d661 100644 --- a/styles.css +++ b/styles.css @@ -415,6 +415,18 @@ input[type="checkbox"] { width: auto; margin: 0 8px 0 0; } align-items: center; } .sideSwitcher button { padding: 6px 7px; font-size: 11px; } +.compactEditorTop { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} +.sideSwitcherLabel { + font-size: 10px; + font-weight: 900; + color: #334155; + text-transform: uppercase; +} .toolRow { display: grid; @@ -2931,6 +2943,8 @@ body, button, input, select, textarea { font-size: 15px; } font-weight: 900 !important; letter-spacing: .04em; cursor: pointer; + pointer-events: auto !important; + touch-action: manipulation; } .placeHereButton[hidden] { display: none !important; } #confirmDialog.reportDialog { position: fixed !important; z-index: 10000 !important; } @@ -2943,7 +2957,7 @@ body, button, input, select, textarea { font-size: 15px; } .basicEditorTools, .studioDrawer.advancedTools .editorToolRow, .studioDrawer.advancedTools .basicEditorTools { - grid-template-columns: repeat(5, minmax(0, 1fr)) !important; + grid-template-columns: repeat(6, minmax(0, 1fr)) !important; } .basicEditorTools #toolBrush { background: linear-gradient(180deg, #dcfce7, #86efac) !important; @@ -2970,3 +2984,9 @@ body, button, input, select, textarea { font-size: 15px; } outline-offset: -6px !important; color: #111827 !important; } +.basicEditorTools #clearPaint { + border-right: 2px solid var(--line, #243044) !important; +} +.studioDrawer .compactEditorTop { + display: flex !important; +}