mirror of
https://github.com/imgfloat/server.git
synced 2026-03-22 23:10:38 +00:00
Fix ordering
This commit is contained in:
@@ -9,6 +9,7 @@ import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.ScriptAssetAttachmentView;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||
@@ -329,8 +330,26 @@ public class ChannelApiController {
|
||||
logBroadcaster,
|
||||
logSessionUsername
|
||||
);
|
||||
return createAsset404();
|
||||
});
|
||||
return createAsset404();
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("/assets/order")
|
||||
public ResponseEntity<Void> reorderAssets(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@Valid @RequestBody AssetOrderRequest request,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
String logBroadcaster = LogSanitizer.sanitize(broadcaster);
|
||||
String logSessionUsername = LogSanitizer.sanitize(sessionUsername);
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
LOG.debug("Reordering assets for {} by {}", logBroadcaster, logSessionUsername);
|
||||
channelDirectoryService.reorderAssets(broadcaster, request.getUpdates(), sessionUsername);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/assets/{assetId}/play")
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package dev.kruhlmann.imgfloat.model.api.request;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public class AssetOrderRequest {
|
||||
|
||||
@Valid
|
||||
@NotEmpty
|
||||
private List<AssetOrderUpdate> updates;
|
||||
|
||||
public List<AssetOrderUpdate> getUpdates() {
|
||||
return updates;
|
||||
}
|
||||
|
||||
public void setUpdates(List<AssetOrderUpdate> updates) {
|
||||
this.updates = updates;
|
||||
}
|
||||
|
||||
public static record AssetOrderUpdate(
|
||||
@NotBlank
|
||||
String assetId,
|
||||
@NotNull
|
||||
Integer order
|
||||
) {}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AssetType;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.ChannelScriptSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.AssetOrderRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.TransformRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.VisibilityRequest;
|
||||
@@ -1157,9 +1159,132 @@ public class ChannelDirectoryService {
|
||||
formatVisualTransformDetails(asset.getId(), req)
|
||||
);
|
||||
}
|
||||
publishOrderUpdates(broadcaster, asset.getId(), orderUpdates);
|
||||
return view;
|
||||
});
|
||||
publishOrderUpdates(broadcaster, asset.getId(), orderUpdates);
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void reorderAssets(
|
||||
String broadcaster,
|
||||
List<AssetOrderRequest.AssetOrderUpdate> updates,
|
||||
String actor
|
||||
) {
|
||||
if (updates == null || updates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String normalized = normalize(broadcaster);
|
||||
applyBulkOrderUpdates(
|
||||
broadcaster,
|
||||
normalized,
|
||||
updates,
|
||||
EnumSet.of(AssetType.SCRIPT),
|
||||
actor,
|
||||
true
|
||||
);
|
||||
applyBulkOrderUpdates(
|
||||
broadcaster,
|
||||
normalized,
|
||||
updates,
|
||||
EnumSet.of(AssetType.IMAGE, AssetType.VIDEO, AssetType.MODEL, AssetType.OTHER),
|
||||
actor,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private void applyBulkOrderUpdates(
|
||||
String broadcaster,
|
||||
String normalized,
|
||||
List<AssetOrderRequest.AssetOrderUpdate> updates,
|
||||
EnumSet<AssetType> types,
|
||||
String actor,
|
||||
boolean script
|
||||
) {
|
||||
if (updates == null || updates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Asset> bucket = assetRepository
|
||||
.findByBroadcaster(normalized)
|
||||
.stream()
|
||||
.filter((asset) -> types.contains(asset.getAssetType()))
|
||||
.sorted(
|
||||
Comparator.comparingInt(this::displayOrderValue)
|
||||
.reversed()
|
||||
.thenComparing(Asset::getCreatedAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||
)
|
||||
.toList();
|
||||
if (bucket.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Integer> desiredOrder = new HashMap<>();
|
||||
Set<String> bucketIds = bucket.stream().map(Asset::getId).collect(Collectors.toSet());
|
||||
for (AssetOrderRequest.AssetOrderUpdate update : updates) {
|
||||
if (update == null) {
|
||||
continue;
|
||||
}
|
||||
String assetId = update.assetId();
|
||||
if (!bucketIds.contains(assetId)) {
|
||||
continue;
|
||||
}
|
||||
Integer order = update.order();
|
||||
if (order == null) {
|
||||
continue;
|
||||
}
|
||||
if (order < 1) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Order must be >= 1");
|
||||
}
|
||||
desiredOrder.put(assetId, order);
|
||||
}
|
||||
if (desiredOrder.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Integer> originalIndex = new HashMap<>();
|
||||
for (int index = 0; index < bucket.size(); index++) {
|
||||
originalIndex.put(bucket.get(index).getId(), index);
|
||||
}
|
||||
List<Asset> ordered = new ArrayList<>(bucket);
|
||||
ordered.sort((a, b) -> {
|
||||
int orderA = desiredOrder.getOrDefault(a.getId(), a.getDisplayOrder() != null ? a.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(a.getId(), bucket.size()));
|
||||
int orderB = desiredOrder.getOrDefault(b.getId(), b.getDisplayOrder() != null ? b.getDisplayOrder() : bucket.size() - originalIndex.getOrDefault(b.getId(), bucket.size()));
|
||||
int cmp = Integer.compare(orderB, orderA);
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
return Integer.compare(originalIndex.getOrDefault(a.getId(), Integer.MAX_VALUE), originalIndex.getOrDefault(b.getId(), Integer.MAX_VALUE));
|
||||
});
|
||||
List<Asset> changed = new ArrayList<>();
|
||||
for (int index = 0; index < ordered.size(); index++) {
|
||||
Asset asset = ordered.get(index);
|
||||
int nextOrder = ordered.size() - index;
|
||||
if (asset.getDisplayOrder() == null || asset.getDisplayOrder() != nextOrder) {
|
||||
asset.setDisplayOrder(nextOrder);
|
||||
changed.add(asset);
|
||||
}
|
||||
}
|
||||
if (changed.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
assetRepository.saveAll(changed);
|
||||
publishOrderUpdates(broadcaster, null, changed);
|
||||
for (Asset asset : changed) {
|
||||
if (script) {
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"SCRIPT_ORDER_UPDATED",
|
||||
formatScriptTransformDetails(asset.getId(), asset.getDisplayOrder())
|
||||
);
|
||||
} else {
|
||||
TransformRequest logDetails = new TransformRequest();
|
||||
logDetails.setOrder(asset.getDisplayOrder());
|
||||
auditLogService.recordEntry(
|
||||
asset.getBroadcaster(),
|
||||
actor,
|
||||
"VISUAL_UPDATED",
|
||||
formatVisualTransformDetails(asset.getId(), logDetails)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVisualTransform(TransformRequest req) {
|
||||
@@ -1171,13 +1296,13 @@ public class ChannelDirectoryService {
|
||||
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
|
||||
|
||||
if (
|
||||
req.getWidth() == null || req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels
|
||||
req.getWidth() != null && (req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels)
|
||||
) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Canvas width out of range [0 to " + canvasMaxSizePixels + "]"
|
||||
);
|
||||
if (
|
||||
req.getHeight() == null || req.getHeight() <= 0 || req.getHeight() > canvasMaxSizePixels
|
||||
req.getHeight() != null && (req.getHeight() <= 0 || req.getHeight() > canvasMaxSizePixels)
|
||||
) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Canvas height out of range [0 to " + canvasMaxSizePixels + "]"
|
||||
|
||||
@@ -26,6 +26,7 @@ export function createAdminConsole({
|
||||
const assets = new Map();
|
||||
const mediaCache = new Map();
|
||||
const renderStates = new Map();
|
||||
const transformBaseline = new Map();
|
||||
const animatedCache = new Map();
|
||||
const audioControllers = new Map();
|
||||
const pendingAudioUnlock = new Set();
|
||||
@@ -506,7 +507,7 @@ export function createAdminConsole({
|
||||
event.preventDefault();
|
||||
updateRenderState(asset);
|
||||
schedulePersistTransform(asset);
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
});
|
||||
function connect() {
|
||||
@@ -621,6 +622,7 @@ export function createAdminConsole({
|
||||
if (!renderStates.has(asset.id)) {
|
||||
renderStates.set(asset.id, { ...merged });
|
||||
}
|
||||
updateTransformBaseline(merged);
|
||||
resolvePendingUploadByName(asset.name);
|
||||
}
|
||||
|
||||
@@ -635,6 +637,35 @@ export function createAdminConsole({
|
||||
renderStates.set(asset.id, state);
|
||||
}
|
||||
|
||||
function updateTransformBaseline(asset) {
|
||||
if (!asset?.id) {
|
||||
return;
|
||||
}
|
||||
const snapshot = {};
|
||||
snapshot.audioVolume = Number.isFinite(asset.audioVolume) ? asset.audioVolume : undefined;
|
||||
if (isAudioAsset(asset)) {
|
||||
snapshot.audioLoop = asset.audioLoop;
|
||||
snapshot.audioDelayMillis = Number.isFinite(asset.audioDelayMillis)
|
||||
? asset.audioDelayMillis
|
||||
: undefined;
|
||||
snapshot.audioSpeed = Number.isFinite(asset.audioSpeed) ? asset.audioSpeed : undefined;
|
||||
snapshot.audioPitch = Number.isFinite(asset.audioPitch) ? asset.audioPitch : undefined;
|
||||
} else {
|
||||
snapshot.x = Number.isFinite(asset.x) ? asset.x : undefined;
|
||||
snapshot.y = Number.isFinite(asset.y) ? asset.y : undefined;
|
||||
snapshot.width = Number.isFinite(asset.width) ? asset.width : undefined;
|
||||
snapshot.height = Number.isFinite(asset.height) ? asset.height : undefined;
|
||||
snapshot.rotation = Number.isFinite(asset.rotation) ? asset.rotation : undefined;
|
||||
snapshot.speed = Number.isFinite(asset.speed) ? asset.speed : undefined;
|
||||
snapshot.muted = asset.muted;
|
||||
const order = isCodeAsset(asset) ? getScriptLayerValue(asset.id) : getLayerValue(asset.id);
|
||||
if (Number.isFinite(order)) {
|
||||
snapshot.order = order;
|
||||
}
|
||||
}
|
||||
transformBaseline.set(asset.id, snapshot);
|
||||
}
|
||||
|
||||
function handleEvent(event) {
|
||||
if (event.type === "CANVAS" && event.payload) {
|
||||
applyCanvasSettings(event.payload);
|
||||
@@ -647,6 +678,7 @@ export function createAdminConsole({
|
||||
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== assetId);
|
||||
clearMedia(assetId);
|
||||
renderStates.delete(assetId);
|
||||
transformBaseline.delete(assetId);
|
||||
loopPlaybackState.delete(assetId);
|
||||
cancelPendingTransform(assetId);
|
||||
if (selectedAssetId === assetId) {
|
||||
@@ -666,7 +698,27 @@ export function createAdminConsole({
|
||||
loopPlaybackState.delete(event.payload.id);
|
||||
}
|
||||
}
|
||||
drawAndList();
|
||||
drawAndList(shouldRenderAssetList(event));
|
||||
}
|
||||
|
||||
function shouldRenderAssetList(event) {
|
||||
if (!event) {
|
||||
return true;
|
||||
}
|
||||
const { type, payload, patch } = event;
|
||||
if (type === "DELETED" || type === "VISIBILITY") {
|
||||
return true;
|
||||
}
|
||||
if (payload) {
|
||||
return true;
|
||||
}
|
||||
if (patch) {
|
||||
if (patch.hidden != null || patch.order != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyPatch(assetId, patch) {
|
||||
@@ -706,9 +758,11 @@ export function createAdminConsole({
|
||||
}
|
||||
}
|
||||
|
||||
function drawAndList() {
|
||||
function drawAndList(renderList = true) {
|
||||
requestDraw();
|
||||
renderAssetList();
|
||||
if (renderList) {
|
||||
renderAssetList();
|
||||
}
|
||||
}
|
||||
|
||||
function requestDraw() {
|
||||
@@ -2055,7 +2109,7 @@ export function createAdminConsole({
|
||||
if (media) {
|
||||
applyMediaSettings(media, asset);
|
||||
}
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
|
||||
function updateVolumeFromInput() {
|
||||
@@ -2074,7 +2128,7 @@ export function createAdminConsole({
|
||||
applyAudioSettings(controller, asset);
|
||||
}
|
||||
schedulePersistTransform(asset);
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
|
||||
function updateAudioSettingsFromInputs() {
|
||||
@@ -2104,7 +2158,7 @@ export function createAdminConsole({
|
||||
const controller = ensureAudioController(asset);
|
||||
applyAudioSettings(controller, asset);
|
||||
schedulePersistTransform(asset);
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
|
||||
function nudgeRotation(delta) {
|
||||
@@ -2114,7 +2168,7 @@ export function createAdminConsole({
|
||||
asset.rotation = next;
|
||||
updateRenderState(asset);
|
||||
persistTransform(asset);
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
|
||||
function recenterSelectedAsset() {
|
||||
@@ -2126,7 +2180,7 @@ export function createAdminConsole({
|
||||
asset.y = centerY;
|
||||
updateRenderState(asset);
|
||||
persistTransform(asset);
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
|
||||
function getLayeredAssets(asset) {
|
||||
@@ -2195,22 +2249,92 @@ export function createAdminConsole({
|
||||
globalThis.sendToBack = sendToBack;
|
||||
|
||||
function applyLayerOrder(ordered) {
|
||||
if (!ordered || !ordered.length) {
|
||||
return;
|
||||
}
|
||||
const previousOrder = [...layerOrder];
|
||||
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
||||
layerOrder = newOrder;
|
||||
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
||||
changed.forEach((item) => updateRenderState(item));
|
||||
changed.forEach((item) => schedulePersistTransform(item, true));
|
||||
const orderUpdates = buildOrderUpdates(ordered, previousOrder);
|
||||
sendOrderUpdates(orderUpdates, () => {
|
||||
layerOrder = previousOrder;
|
||||
drawAndList();
|
||||
});
|
||||
drawAndList();
|
||||
}
|
||||
|
||||
function applyScriptLayerOrder(ordered) {
|
||||
if (!ordered || !ordered.length) {
|
||||
return;
|
||||
}
|
||||
const previousOrder = [...scriptLayerOrder];
|
||||
const newOrder = ordered.map((item) => item.id).filter((id) => assets.has(id));
|
||||
scriptLayerOrder = newOrder;
|
||||
const changed = ordered.map((item) => assets.get(item.id)).filter(Boolean);
|
||||
changed.forEach((item) => schedulePersistTransform(item, true));
|
||||
const orderUpdates = buildOrderUpdates(ordered, previousOrder);
|
||||
sendOrderUpdates(orderUpdates, () => {
|
||||
scriptLayerOrder = previousOrder;
|
||||
drawAndList();
|
||||
});
|
||||
drawAndList();
|
||||
}
|
||||
|
||||
function buildOrderUpdates(ordered, previousOrderIds) {
|
||||
if (!ordered || !ordered.length) {
|
||||
return [];
|
||||
}
|
||||
const previousLength = previousOrderIds.length || ordered.length;
|
||||
const previousOrderMap = new Map();
|
||||
previousOrderIds.forEach((id, index) => {
|
||||
previousOrderMap.set(id, previousLength - index);
|
||||
});
|
||||
const updates = [];
|
||||
const newLength = ordered.length;
|
||||
ordered.forEach((asset, index) => {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
const newOrderValue = newLength - index;
|
||||
const previousValue = previousOrderMap.get(asset.id);
|
||||
if (previousValue !== newOrderValue) {
|
||||
updates.push({ assetId: asset.id, order: newOrderValue });
|
||||
}
|
||||
});
|
||||
return updates;
|
||||
}
|
||||
|
||||
function sendOrderUpdates(updates, onError) {
|
||||
if (!updates || !updates.length) {
|
||||
return;
|
||||
}
|
||||
fetch(`/api/channels/${broadcaster}/assets/order`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ updates }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return extractErrorMessage(response, "Unable to reorder assets right now.").then((message) => {
|
||||
throw new Error(message);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (onError) {
|
||||
onError();
|
||||
}
|
||||
const assetDetails = updates
|
||||
.map((update) => {
|
||||
const asset = assets.get(update.assetId);
|
||||
return `${asset?.name ?? "unknown"} (${update.assetId})`;
|
||||
})
|
||||
.join(", ");
|
||||
console.warn("Asset reorder failed for", assetDetails, error?.message || error);
|
||||
showToast(error?.message || "Unable to reorder assets right now.", "error");
|
||||
});
|
||||
}
|
||||
|
||||
function getAssetAspectRatio(asset) {
|
||||
const media = ensureMedia(asset);
|
||||
if (isVideoElement(media) && media?.videoWidth && media?.videoHeight) {
|
||||
@@ -2315,12 +2439,13 @@ export function createAdminConsole({
|
||||
throw new Error(message);
|
||||
});
|
||||
}
|
||||
clearMedia(asset.id);
|
||||
assets.delete(asset.id);
|
||||
renderStates.delete(asset.id);
|
||||
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
||||
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== asset.id);
|
||||
cancelPendingTransform(asset.id);
|
||||
clearMedia(asset.id);
|
||||
assets.delete(asset.id);
|
||||
renderStates.delete(asset.id);
|
||||
transformBaseline.delete(asset.id);
|
||||
layerOrder = layerOrder.filter((id) => id !== asset.id);
|
||||
scriptLayerOrder = scriptLayerOrder.filter((id) => id !== asset.id);
|
||||
cancelPendingTransform(asset.id);
|
||||
if (selectedAssetId === asset.id) {
|
||||
selectedAssetId = null;
|
||||
}
|
||||
@@ -2452,29 +2577,13 @@ export function createAdminConsole({
|
||||
}
|
||||
|
||||
function persistTransform(asset, silent = false) {
|
||||
if (!asset || !asset.id) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
cancelPendingTransform(asset.id);
|
||||
const payload = {
|
||||
audioLoop: asset.audioLoop,
|
||||
audioDelayMillis: asset.audioDelayMillis,
|
||||
audioSpeed: asset.audioSpeed,
|
||||
audioPitch: asset.audioPitch,
|
||||
audioVolume: asset.audioVolume,
|
||||
};
|
||||
if (isCodeAsset(asset)) {
|
||||
const order = getScriptLayerValue(asset.id);
|
||||
payload.order = order;
|
||||
} else if (!isAudioAsset(asset)) {
|
||||
const order = getLayerValue(asset.id);
|
||||
payload.x = asset.x;
|
||||
payload.y = asset.y;
|
||||
payload.width = asset.width;
|
||||
payload.height = asset.height;
|
||||
payload.rotation = asset.rotation;
|
||||
payload.speed = asset.speed;
|
||||
payload.order = order;
|
||||
if (isVideoAsset(asset)) {
|
||||
payload.muted = asset.muted;
|
||||
}
|
||||
const payload = buildTransformPayload(asset);
|
||||
if (!Object.keys(payload).length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
|
||||
method: "PUT",
|
||||
@@ -2493,7 +2602,7 @@ export function createAdminConsole({
|
||||
storeAsset(updated);
|
||||
updateRenderState(updated);
|
||||
if (!silent) {
|
||||
drawAndList();
|
||||
drawAndList(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -2503,6 +2612,57 @@ export function createAdminConsole({
|
||||
});
|
||||
}
|
||||
|
||||
function buildTransformPayload(asset) {
|
||||
if (!asset?.id) {
|
||||
return {};
|
||||
}
|
||||
const baseline = transformBaseline.get(asset.id) || {};
|
||||
const payload = {};
|
||||
const addNumber = (key, value) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
const previous = baseline[key];
|
||||
if (Number.isFinite(previous) && previous === value) {
|
||||
return;
|
||||
}
|
||||
payload[key] = value;
|
||||
};
|
||||
const addBoolean = (key, value) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (baseline[key] === value) {
|
||||
return;
|
||||
}
|
||||
payload[key] = value;
|
||||
};
|
||||
|
||||
addBoolean("audioLoop", asset.audioLoop);
|
||||
addNumber("audioDelayMillis", asset.audioDelayMillis);
|
||||
addNumber("audioSpeed", asset.audioSpeed);
|
||||
addNumber("audioPitch", asset.audioPitch);
|
||||
addNumber("audioVolume", asset.audioVolume);
|
||||
|
||||
if (!isAudioAsset(asset)) {
|
||||
addNumber("x", asset.x);
|
||||
addNumber("y", asset.y);
|
||||
addNumber("width", asset.width);
|
||||
addNumber("height", asset.height);
|
||||
addNumber("rotation", asset.rotation);
|
||||
addNumber("speed", asset.speed);
|
||||
if (isVideoAsset(asset)) {
|
||||
addBoolean("muted", asset.muted);
|
||||
}
|
||||
const order = isCodeAsset(asset) ? getScriptLayerValue(asset.id) : getLayerValue(asset.id);
|
||||
if (Number.isFinite(order) && baseline.order !== order) {
|
||||
payload.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
canvas.addEventListener("mousedown", (event) => {
|
||||
const point = getCanvasPoint(event);
|
||||
const current = getSelectedAsset();
|
||||
|
||||
@@ -10,24 +10,18 @@ export function getVisibilityState(state, asset) {
|
||||
}
|
||||
|
||||
export function smoothState(state, asset) {
|
||||
const previous = state.renderStates.get(asset.id) || { ...asset };
|
||||
const factor = 0.15;
|
||||
const previous = state.renderStates.get(asset.id) || {};
|
||||
const next = {
|
||||
x: lerp(previous.x, asset.x, factor),
|
||||
y: lerp(previous.y, asset.y, factor),
|
||||
width: lerp(previous.width, asset.width, factor),
|
||||
height: lerp(previous.height, asset.height, factor),
|
||||
rotation: smoothAngle(previous.rotation, asset.rotation, factor),
|
||||
x: Number.isFinite(asset.x) ? asset.x : previous.x ?? 0,
|
||||
y: Number.isFinite(asset.y) ? asset.y : previous.y ?? 0,
|
||||
width: Number.isFinite(asset.width) ? asset.width : previous.width ?? 0,
|
||||
height: Number.isFinite(asset.height) ? asset.height : previous.height ?? 0,
|
||||
rotation: Number.isFinite(asset.rotation) ? asset.rotation : previous.rotation ?? 0,
|
||||
};
|
||||
state.renderStates.set(asset.id, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
function smoothAngle(current, target, factor) {
|
||||
const delta = ((target - current + 180) % 360) - 180;
|
||||
return current + delta * factor;
|
||||
}
|
||||
|
||||
function lerp(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user