Fix settings

This commit is contained in:
2025-12-10 23:37:08 +01:00
parent d68538d285
commit 96f497c20a
2 changed files with 239 additions and 110 deletions

View File

@@ -16,8 +16,7 @@ const loopPlaybackState = new Map();
const previewCache = new Map(); const previewCache = new Map();
const previewImageCache = new Map(); const previewImageCache = new Map();
let drawPending = false; let drawPending = false;
let zOrderDirty = true; let layerOrder = [];
let zOrderCache = [];
let selectedAssetId = null; let selectedAssetId = null;
let interactionState = null; let interactionState = null;
let lastSizeInputChanged = null; let lastSizeInputChanged = null;
@@ -26,6 +25,7 @@ const ROTATE_HANDLE_OFFSET = 32;
const MAX_VOLUME = 2; const MAX_VOLUME = 2;
const VOLUME_SLIDER_MAX = 200; const VOLUME_SLIDER_MAX = 200;
const VOLUME_CURVE_STRENGTH = -0.6; const VOLUME_CURVE_STRENGTH = -0.6;
const pendingTransformSaves = new Map();
const controlsPanel = document.getElementById('asset-controls'); const controlsPanel = document.getElementById('asset-controls');
@@ -84,6 +84,85 @@ function debounce(fn, wait = 150) {
}; };
} }
function schedulePersistTransform(asset, silent = false, delay = 200) {
if (!asset?.id) return;
cancelPendingTransform(asset.id);
const timeout = setTimeout(() => {
pendingTransformSaves.delete(asset.id);
persistTransform(asset, silent);
}, delay);
pendingTransformSaves.set(asset.id, timeout);
}
function cancelPendingTransform(assetId) {
const pending = pendingTransformSaves.get(assetId);
if (pending) {
clearTimeout(pending);
pendingTransformSaves.delete(assetId);
}
}
function ensureLayerPosition(assetId, placement = 'keep') {
const asset = assets.get(assetId);
if (asset && isAudioAsset(asset)) {
return;
}
const existingIndex = layerOrder.indexOf(assetId);
if (existingIndex !== -1 && placement === 'keep') {
return;
}
if (existingIndex !== -1) {
layerOrder.splice(existingIndex, 1);
}
if (placement === 'append') {
layerOrder.push(assetId);
} else {
layerOrder.unshift(assetId);
}
layerOrder = layerOrder.filter((id) => assets.has(id));
}
function getLayerOrder() {
layerOrder = layerOrder.filter((id) => {
const asset = assets.get(id);
return asset && !isAudioAsset(asset);
});
assets.forEach((asset, id) => {
if (isAudioAsset(asset)) {
return;
}
if (!layerOrder.includes(id)) {
layerOrder.unshift(id);
}
});
return layerOrder;
}
function getAssetsByLayer() {
return getLayerOrder().map((id) => assets.get(id)).filter(Boolean);
}
function getAudioAssets() {
return Array.from(assets.values())
.filter((asset) => isAudioAsset(asset))
.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
}
function getRenderOrder() {
return [...getLayerOrder()].reverse().map((id) => assets.get(id)).filter(Boolean);
}
function getLayerValue(assetId) {
const asset = assets.get(assetId);
if (asset && isAudioAsset(asset)) {
return 0;
}
const order = getLayerOrder();
const index = order.indexOf(assetId);
if (index === -1) return 1;
return order.length - index;
}
function addPendingUpload(name) { function addPendingUpload(name) {
const pending = { const pending = {
id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`, id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`,
@@ -263,13 +342,6 @@ if (audioPitchInput) audioPitchInput.addEventListener('input', () => {
setAudioPitchLabel(audioPitchInput.value); setAudioPitchLabel(audioPitchInput.value);
updateAudioSettingsFromInputs(); updateAudioSettingsFromInputs();
}); });
if (selectedVisibilityBtn) {
selectedVisibilityBtn.addEventListener('click', () => {
const asset = getSelectedAsset();
if (!asset) return;
updateVisibility(asset, !asset.hidden);
});
}
if (selectedDeleteBtn) { if (selectedDeleteBtn) {
selectedDeleteBtn.addEventListener('click', () => { selectedDeleteBtn.addEventListener('click', () => {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
@@ -354,12 +426,14 @@ function resizeCanvas() {
} }
function renderAssets(list) { function renderAssets(list) {
list.forEach(storeAsset); layerOrder = [];
list.forEach((item) => storeAsset(item, { placement: 'append' }));
drawAndList(); drawAndList();
} }
function storeAsset(asset) { function storeAsset(asset, options = {}) {
if (!asset) return; if (!asset) return;
const placement = options.placement || 'keep';
const existing = assets.get(asset.id); const existing = assets.get(asset.id);
const merged = existing ? { ...existing, ...asset } : { ...asset }; const merged = existing ? { ...existing, ...asset } : { ...asset };
const mediaChanged = existing && existing.url !== merged.url; const mediaChanged = existing && existing.url !== merged.url;
@@ -367,14 +441,13 @@ function storeAsset(asset) {
if (mediaChanged || previewChanged) { if (mediaChanged || previewChanged) {
clearMedia(asset.id); clearMedia(asset.id);
} }
merged.zIndex = Math.max(1, merged.zIndex ?? 1);
const parsedCreatedAt = merged.createdAt ? new Date(merged.createdAt).getTime() : NaN; const parsedCreatedAt = merged.createdAt ? new Date(merged.createdAt).getTime() : NaN;
const hasCreatedAtMs = typeof merged.createdAtMs === 'number' && Number.isFinite(merged.createdAtMs); const hasCreatedAtMs = typeof merged.createdAtMs === 'number' && Number.isFinite(merged.createdAtMs);
if (!hasCreatedAtMs) { if (!hasCreatedAtMs) {
merged.createdAtMs = Number.isFinite(parsedCreatedAt) ? parsedCreatedAt : Date.now(); merged.createdAtMs = Number.isFinite(parsedCreatedAt) ? parsedCreatedAt : Date.now();
} }
assets.set(asset.id, merged); assets.set(asset.id, merged);
zOrderDirty = true; ensureLayerPosition(asset.id, existing ? 'keep' : placement);
if (!renderStates.has(asset.id)) { if (!renderStates.has(asset.id)) {
renderStates.set(asset.id, { ...merged }); renderStates.set(asset.id, { ...merged });
} }
@@ -396,10 +469,11 @@ function handleEvent(event) {
const assetId = event.assetId || event?.patch?.id || event?.payload?.id; const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
if (event.type === 'DELETED') { if (event.type === 'DELETED') {
assets.delete(assetId); assets.delete(assetId);
zOrderDirty = true; layerOrder = layerOrder.filter((id) => id !== assetId);
clearMedia(assetId); clearMedia(assetId);
renderStates.delete(assetId); renderStates.delete(assetId);
loopPlaybackState.delete(assetId); loopPlaybackState.delete(assetId);
cancelPendingTransform(assetId);
if (selectedAssetId === assetId) { if (selectedAssetId === assetId) {
selectedAssetId = null; selectedAssetId = null;
} }
@@ -429,13 +503,25 @@ function applyPatch(assetId, patch) {
return; return;
} }
const merged = { ...existing, ...patch }; const merged = { ...existing, ...patch };
const isAudio = isAudioAsset(merged);
if (patch.hidden) { if (patch.hidden) {
clearMedia(assetId); clearMedia(assetId);
loopPlaybackState.delete(assetId); loopPlaybackState.delete(assetId);
} }
const targetLayer = Number.isFinite(patch.layer)
? patch.layer
: (Number.isFinite(patch.zIndex) ? patch.zIndex : null);
if (!isAudio && Number.isFinite(targetLayer)) {
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
currentOrder.splice(insertIndex, 0, assetId);
layerOrder = currentOrder;
}
storeAsset(merged); storeAsset(merged);
if (!isAudio) {
updateRenderState(merged); updateRenderState(merged);
} }
}
function drawAndList() { function drawAndList() {
requestDraw(); requestDraw();
@@ -455,28 +541,7 @@ function requestDraw() {
function draw() { function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
getZOrderedAssets().forEach((asset) => drawAsset(asset)); getRenderOrder().forEach((asset) => drawAsset(asset));
}
function getZOrderedAssets() {
if (zOrderDirty) {
zOrderCache = Array.from(assets.values()).sort(zComparator);
zOrderDirty = false;
}
return zOrderCache;
}
function zComparator(a, b) {
const aZ = a?.zIndex ?? 1;
const bZ = b?.zIndex ?? 1;
if (aZ !== bZ) {
return aZ - bZ;
}
return (a?.createdAtMs || 0) - (b?.createdAtMs || 0);
}
function getChronologicalAssets() {
return Array.from(assets.values()).sort((a, b) => (a?.createdAtMs || 0) - (b?.createdAtMs || 0));
} }
function drawAsset(asset) { function drawAsset(asset) {
@@ -1096,7 +1161,8 @@ function renderAssetList() {
list.appendChild(createPendingListItem(pending)); list.appendChild(createPendingListItem(pending));
}); });
const sortedAssets = getChronologicalAssets(); const audioAssets = getAudioAssets();
const sortedAssets = [...audioAssets, ...getAssetsByLayer()];
sortedAssets.forEach((asset) => { sortedAssets.forEach((asset) => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'asset-item'; li.className = 'asset-item';
@@ -1119,6 +1185,16 @@ function renderAssetList() {
meta.appendChild(name); meta.appendChild(name);
meta.appendChild(details); meta.appendChild(details);
const badges = document.createElement('div');
badges.className = 'badge-row asset-detail';
const durationLabel = getDurationBadge(asset);
if (durationLabel) {
badges.appendChild(createBadge(durationLabel, 'subtle'));
}
if (badges.children.length > 0) {
meta.appendChild(badges);
}
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'actions'; actions.className = 'actions';
@@ -1436,7 +1512,7 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
controlsPanel.classList.remove('hidden'); controlsPanel.classList.remove('hidden');
lastSizeInputChanged = null; lastSizeInputChanged = null;
if (selectedZLabel) { if (selectedZLabel) {
selectedZLabel.textContent = asset.zIndex ?? 1; selectedZLabel.textContent = getLayerValue(asset.id);
} }
if (widthInput) widthInput.value = Math.round(asset.width); if (widthInput) widthInput.value = Math.round(asset.width);
@@ -1545,9 +1621,6 @@ function updateSelectedAssetSummary(asset) {
selectedAssetBadges.innerHTML = ''; selectedAssetBadges.innerHTML = '';
if (asset) { if (asset) {
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset))); selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
if (!isAudioAsset(asset)) {
selectedAssetBadges.appendChild(createBadge(asset.hidden ? 'Hidden' : 'Visible', asset.hidden ? 'danger' : ''));
}
const aspectLabel = !isAudioAsset(asset) ? formatAspectRatioLabel(asset) : ''; const aspectLabel = !isAudioAsset(asset) ? formatAspectRatioLabel(asset) : '';
if (aspectLabel) { if (aspectLabel) {
selectedAssetBadges.appendChild(createBadge(aspectLabel, 'subtle')); selectedAssetBadges.appendChild(createBadge(aspectLabel, 'subtle'));
@@ -1560,10 +1633,33 @@ function updateSelectedAssetSummary(asset) {
} }
if (selectedVisibilityBtn) { if (selectedVisibilityBtn) {
selectedVisibilityBtn.disabled = !asset; selectedVisibilityBtn.disabled = !asset;
selectedVisibilityBtn.title = asset ? (asset.hidden ? 'Show asset' : 'Hide asset') : 'Toggle visibility'; selectedVisibilityBtn.onclick = null;
selectedVisibilityBtn.innerHTML = asset if (asset && isAudioAsset(asset)) {
? `<i class="fa-solid ${asset.hidden ? 'fa-eye' : 'fa-eye-slash'}"></i>` const isLooping = !!asset.audioLoop;
: '<i class="fa-solid fa-eye-slash"></i>'; const isPlayingLoop = getLoopPlaybackState(asset);
updatePlayButtonIcon(selectedVisibilityBtn, isLooping, isPlayingLoop);
selectedVisibilityBtn.title = isLooping
? (isPlayingLoop ? 'Pause looping audio' : 'Play looping audio')
: 'Play audio';
selectedVisibilityBtn.onclick = () => {
const nextPlay = isLooping
? !(loopPlaybackState.get(asset.id) ?? getLoopPlaybackState(asset))
: true;
if (isLooping) {
loopPlaybackState.set(asset.id, nextPlay);
updatePlayButtonIcon(selectedVisibilityBtn, true, nextPlay);
selectedVisibilityBtn.title = nextPlay ? 'Pause looping audio' : 'Play looping audio';
}
triggerAudioPlayback(asset, nextPlay);
};
} else if (asset) {
selectedVisibilityBtn.title = asset.hidden ? 'Show asset' : 'Hide asset';
selectedVisibilityBtn.innerHTML = `<i class="fa-solid ${asset.hidden ? 'fa-eye' : 'fa-eye-slash'}"></i>`;
selectedVisibilityBtn.onclick = () => updateVisibility(asset, !asset.hidden);
} else {
selectedVisibilityBtn.title = 'Toggle visibility';
selectedVisibilityBtn.innerHTML = '<i class="fa-solid fa-eye-slash"></i>';
}
} }
if (selectedDeleteBtn) { if (selectedDeleteBtn) {
selectedDeleteBtn.disabled = !asset; selectedDeleteBtn.disabled = !asset;
@@ -1603,7 +1699,7 @@ function updatePlaybackFromInputs() {
setSpeedLabel(percent); setSpeedLabel(percent);
asset.speed = percent / 100; asset.speed = percent / 100;
updateRenderState(asset); updateRenderState(asset);
persistTransform(asset); schedulePersistTransform(asset);
const media = mediaCache.get(asset.id); const media = mediaCache.get(asset.id);
if (media) { if (media) {
applyMediaSettings(media, asset); applyMediaSettings(media, asset);
@@ -1626,7 +1722,7 @@ function updateVolumeFromInput() {
const controller = ensureAudioController(asset); const controller = ensureAudioController(asset);
applyAudioSettings(controller, asset); applyAudioSettings(controller, asset);
} }
persistTransform(asset); schedulePersistTransform(asset);
drawAndList(); drawAndList();
} }
@@ -1648,7 +1744,7 @@ function updateAudioSettingsFromInputs() {
asset.audioPitch = Math.max(0.5, (nextAudioPitchPercent / 100)); asset.audioPitch = Math.max(0.5, (nextAudioPitchPercent / 100));
const controller = ensureAudioController(asset); const controller = ensureAudioController(asset);
applyAudioSettings(controller, asset); applyAudioSettings(controller, asset);
persistTransform(asset); schedulePersistTransform(asset);
drawAndList(); drawAndList();
} }
@@ -1677,52 +1773,45 @@ function recenterSelectedAsset() {
function bringForward() { function bringForward() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset) return; if (!asset) return;
const ordered = [...getZOrderedAssets()]; const ordered = getAssetsByLayer();
const index = ordered.findIndex((item) => item.id === asset.id); const index = ordered.findIndex((item) => item.id === asset.id);
if (index === -1 || index === ordered.length - 1) return; if (index <= 0) return;
[ordered[index], ordered[index + 1]] = [ordered[index + 1], ordered[index]]; [ordered[index], ordered[index - 1]] = [ordered[index - 1], ordered[index]];
applyZOrder(ordered); applyLayerOrder(ordered);
} }
function bringBackward() { function bringBackward() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset) return; if (!asset) return;
const ordered = [...getZOrderedAssets()]; const ordered = getAssetsByLayer();
const index = ordered.findIndex((item) => item.id === asset.id); const index = ordered.findIndex((item) => item.id === asset.id);
if (index <= 0) return; if (index === -1 || index === ordered.length - 1) return;
[ordered[index], ordered[index - 1]] = [ordered[index - 1], ordered[index]]; [ordered[index], ordered[index + 1]] = [ordered[index + 1], ordered[index]];
applyZOrder(ordered); applyLayerOrder(ordered);
} }
function bringToFront() { function bringToFront() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset) return; if (!asset) return;
const ordered = getZOrderedAssets().filter((item) => item.id !== asset.id); const ordered = getAssetsByLayer().filter((item) => item.id !== asset.id);
ordered.push(asset); ordered.unshift(asset);
applyZOrder(ordered); applyLayerOrder(ordered);
} }
function sendToBack() { function sendToBack() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset) return; if (!asset) return;
const ordered = getZOrderedAssets().filter((item) => item.id !== asset.id); const ordered = getAssetsByLayer().filter((item) => item.id !== asset.id);
ordered.unshift(asset); ordered.push(asset);
applyZOrder(ordered); applyLayerOrder(ordered);
} }
function applyZOrder(ordered) { function applyLayerOrder(ordered) {
const changed = []; const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
ordered.forEach((item, index) => { layerOrder = newOrder;
const nextIndex = index + 1; const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
if ((item.zIndex ?? 1) !== nextIndex) { changed.forEach((item) => updateRenderState(item));
item.zIndex = nextIndex; changed.forEach((item) => schedulePersistTransform(item, true));
changed.push(item);
}
assets.set(item.id, item);
updateRenderState(item);
});
zOrderDirty = true;
changed.forEach((item) => persistTransform(item, true));
drawAndList(); drawAndList();
} }
@@ -1845,7 +1934,8 @@ function deleteAsset(asset) {
clearMedia(asset.id); clearMedia(asset.id);
assets.delete(asset.id); assets.delete(asset.id);
renderStates.delete(asset.id); renderStates.delete(asset.id);
zOrderDirty = true; layerOrder = layerOrder.filter((id) => id !== asset.id);
cancelPendingTransform(asset.id);
if (selectedAssetId === asset.id) { if (selectedAssetId === asset.id) {
selectedAssetId = null; selectedAssetId = null;
} }
@@ -1939,12 +2029,13 @@ function isPointOnAsset(asset, x, y) {
} }
function findAssetAtPoint(x, y) { function findAssetAtPoint(x, y) {
const ordered = [...getZOrderedAssets()].reverse(); const ordered = getAssetsByLayer();
return ordered.find((asset) => !isAudioAsset(asset) && isPointOnAsset(asset, x, y)) || null; return ordered.find((asset) => !isAudioAsset(asset) && isPointOnAsset(asset, x, y)) || null;
} }
function persistTransform(asset, silent = false) { function persistTransform(asset, silent = false) {
asset.zIndex = Math.max(1, asset.zIndex ?? 1); cancelPendingTransform(asset.id);
const layer = getLayerValue(asset.id);
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, { fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -1955,7 +2046,8 @@ function persistTransform(asset, silent = false) {
height: asset.height, height: asset.height,
rotation: asset.rotation, rotation: asset.rotation,
speed: asset.speed, speed: asset.speed,
zIndex: asset.zIndex, layer,
zIndex: layer,
audioLoop: asset.audioLoop, audioLoop: asset.audioLoop,
audioDelayMillis: asset.audioDelayMillis, audioDelayMillis: asset.audioDelayMillis,
audioSpeed: asset.audioSpeed, audioSpeed: asset.audioSpeed,

View File

@@ -19,10 +19,9 @@ const MIN_FRAME_TIME = 1000 / TARGET_FPS;
let lastRenderTime = 0; let lastRenderTime = 0;
let frameScheduled = false; let frameScheduled = false;
let pendingDraw = false; let pendingDraw = false;
let sortedAssetsCache = [];
let assetsDirty = true;
let renderIntervalId = null; let renderIntervalId = null;
const audioUnlockEvents = ['pointerdown', 'keydown', 'touchstart']; const audioUnlockEvents = ['pointerdown', 'keydown', 'touchstart'];
let layerOrder = [];
audioUnlockEvents.forEach((eventName) => { audioUnlockEvents.forEach((eventName) => {
window.addEventListener(eventName, () => { window.addEventListener(eventName, () => {
@@ -32,6 +31,46 @@ audioUnlockEvents.forEach((eventName) => {
}); });
}); });
function ensureLayerPosition(assetId, placement = 'keep') {
const asset = assets.get(assetId);
if (asset && isAudioAsset(asset)) {
return;
}
const existingIndex = layerOrder.indexOf(assetId);
if (existingIndex !== -1 && placement === 'keep') {
return;
}
if (existingIndex !== -1) {
layerOrder.splice(existingIndex, 1);
}
if (placement === 'append') {
layerOrder.push(assetId);
} else {
layerOrder.unshift(assetId);
}
layerOrder = layerOrder.filter((id) => assets.has(id));
}
function getLayerOrder() {
layerOrder = layerOrder.filter((id) => {
const asset = assets.get(id);
return asset && !isAudioAsset(asset);
});
assets.forEach((asset, id) => {
if (isAudioAsset(asset)) {
return;
}
if (!layerOrder.includes(id)) {
layerOrder.unshift(id);
}
});
return layerOrder;
}
function getRenderOrder() {
return [...getLayerOrder()].reverse().map((id) => assets.get(id)).filter(Boolean);
}
function connect() { function connect() {
const socket = new SockJS('/ws'); const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket); const stompClient = Stomp.over(socket);
@@ -57,14 +96,17 @@ function connect() {
} }
function renderAssets(list) { function renderAssets(list) {
list.forEach(asset => { layerOrder = [];
asset.zIndex = Math.max(1, asset.zIndex ?? 1); list.forEach((asset) => storeAsset(asset, 'append'));
assets.set(asset.id, asset);
});
assetsDirty = true;
draw(); draw();
} }
function storeAsset(asset, placement = 'keep') {
if (!asset) return;
assets.set(asset.id, asset);
ensureLayerPosition(asset.id, placement);
}
function fetchCanvasSettings() { function fetchCanvasSettings() {
return fetch(`/api/channels/${broadcaster}/canvas`) return fetch(`/api/channels/${broadcaster}/canvas`)
.then((r) => { .then((r) => {
@@ -102,34 +144,35 @@ function handleEvent(event) {
const assetId = event.assetId || event?.patch?.id || event?.payload?.id; const assetId = event.assetId || event?.patch?.id || event?.payload?.id;
if (event.type === 'DELETED') { if (event.type === 'DELETED') {
assets.delete(assetId); assets.delete(assetId);
layerOrder = layerOrder.filter((id) => id !== assetId);
clearMedia(assetId); clearMedia(assetId);
renderStates.delete(assetId); renderStates.delete(assetId);
} else if (event.patch) { } else if (event.patch) {
applyPatch(assetId, event.patch); applyPatch(assetId, event.patch);
} else if (event.type === 'PLAY' && event.payload) { } else if (event.type === 'PLAY' && event.payload) {
const payload = normalizePayload(event.payload); const payload = normalizePayload(event.payload);
assets.set(payload.id, payload); storeAsset(payload);
if (isAudioAsset(payload)) { if (isAudioAsset(payload)) {
handleAudioPlay(payload, event.play !== false); handleAudioPlay(payload, event.play !== false);
} }
} else if (event.payload && !event.payload.hidden) { } else if (event.payload && !event.payload.hidden) {
const payload = normalizePayload(event.payload); const payload = normalizePayload(event.payload);
assets.set(payload.id, payload); storeAsset(payload);
ensureMedia(payload); ensureMedia(payload);
if (isAudioAsset(payload)) { if (isAudioAsset(payload)) {
playAudioImmediately(payload); playAudioImmediately(payload);
} }
} else if (event.payload && event.payload.hidden) { } else if (event.payload && event.payload.hidden) {
assets.delete(event.payload.id); assets.delete(event.payload.id);
layerOrder = layerOrder.filter((id) => id !== event.payload.id);
clearMedia(event.payload.id); clearMedia(event.payload.id);
renderStates.delete(event.payload.id); renderStates.delete(event.payload.id);
} }
assetsDirty = true;
draw(); draw();
} }
function normalizePayload(payload) { function normalizePayload(payload) {
return { ...payload, zIndex: Math.max(1, payload.zIndex ?? 1) }; return { ...payload };
} }
function applyPatch(assetId, patch) { function applyPatch(assetId, patch) {
@@ -141,13 +184,24 @@ function applyPatch(assetId, patch) {
return; return;
} }
const merged = normalizePayload({ ...existing, ...patch }); const merged = normalizePayload({ ...existing, ...patch });
const isAudio = isAudioAsset(merged);
if (patch.hidden) { if (patch.hidden) {
assets.delete(assetId); assets.delete(assetId);
layerOrder = layerOrder.filter((id) => id !== assetId);
clearMedia(assetId); clearMedia(assetId);
renderStates.delete(assetId); renderStates.delete(assetId);
return; return;
} }
assets.set(assetId, merged); const targetLayer = Number.isFinite(patch.layer)
? patch.layer
: (Number.isFinite(patch.zIndex) ? patch.zIndex : null);
if (!isAudio && Number.isFinite(targetLayer)) {
const currentOrder = getLayerOrder().filter((id) => id !== assetId);
const insertIndex = Math.max(0, currentOrder.length - Math.round(targetLayer));
currentOrder.splice(insertIndex, 0, assetId);
layerOrder = currentOrder;
}
storeAsset(merged);
ensureMedia(merged); ensureMedia(merged);
renderStates.set(assetId, { ...renderStates.get(assetId), ...pickTransform(merged) }); renderStates.set(assetId, { ...renderStates.get(assetId), ...pickTransform(merged) });
} }
@@ -188,24 +242,7 @@ function draw() {
function renderFrame() { function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
getZOrderedAssets().forEach(drawAsset); getRenderOrder().forEach(drawAsset);
}
function getZOrderedAssets() {
if (assetsDirty) {
sortedAssetsCache = Array.from(assets.values()).sort(zComparator);
assetsDirty = false;
}
return sortedAssetsCache;
}
function zComparator(a, b) {
const aZ = a?.zIndex ?? 1;
const bZ = b?.zIndex ?? 1;
if (aZ !== bZ) {
return aZ - bZ;
}
return new Date(a?.createdAt || 0) - new Date(b?.createdAt || 0);
} }
function drawAsset(asset) { function drawAsset(asset) {