From 7926ba47935b5a26c663f13e1329560d3ad91ddf Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Sun, 21 Sep 2025 01:43:44 -0300 Subject: [PATCH 1/6] feat: add size templates on dialog new project --- src/components/dialogs/DialogNewProject.ts | 75 +++++++++-- src/components/dialogs/dialog.ts | 122 ++++++++---------- src/components/gradientMenu.ts | 22 ++-- src/components/helpers/createSelectInput.ts | 85 ++++++++---- src/components/helpers/createSliderControl.ts | 101 +++++++++++---- src/components/helpers/createTextInput.ts | 26 ++-- src/components/textMenu.ts | 24 ++-- src/components/transformMenu.ts | 96 +++++++------- src/components/types.ts | 5 - src/filters/colorCorrectionFilter.ts | 10 +- src/filters/dropShadowFilter.ts | 6 +- src/filters/filter.ts | 6 +- src/filters/outerGlowFilter.ts | 2 +- src/templates.ts | 12 ++ 14 files changed, 364 insertions(+), 228 deletions(-) create mode 100644 src/templates.ts diff --git a/src/components/dialogs/DialogNewProject.ts b/src/components/dialogs/DialogNewProject.ts index 940da3b..60ce33c 100644 --- a/src/components/dialogs/DialogNewProject.ts +++ b/src/components/dialogs/DialogNewProject.ts @@ -5,6 +5,9 @@ import type { ISliderControl } from "../helpers/createSliderControl"; import createSliderControl from "../helpers/createSliderControl"; import type { ITextInput } from "../helpers/createTextInput"; import createTextInput from "../helpers/createTextInput"; +import type { ISelectInput } from "../helpers/createSelectInput"; +import createSelectInput from "../helpers/createSelectInput"; +import { templates } from "src/templates"; const WORK_AREA_WIDTH = 1920; const WORK_AREA_HEIGHT = 1080; @@ -15,6 +18,7 @@ export class DialogNewProject extends Dialog { private workAreaWidthInput: ISliderControl | null = null; private workAreaHeightInput: ISliderControl | null = null; private projectNameInput: ITextInput | null = null; + private templatesInput: ISelectInput | null = null; constructor(eventBus: EventBus) { super({ @@ -52,44 +56,89 @@ export class DialogNewProject extends Dialog { } } + private handleTemplateChange(newValue: string): void { + if (!this.workAreaWidthInput || !this.workAreaHeightInput) { + console.log("[DialogNewProject] ignorando mudança de template, sliders ainda não criados"); + return; + } + const tpl = templates.find((t) => t.name === newValue); + if (!tpl) { + console.warn("[DialogNewProject] Template não encontrado:", newValue); + return; + } + + const width = tpl.width; + const height = tpl.height; + this.workAreaWidthInput?.setValue(width); + this.workAreaHeightInput?.setValue(height); + this.handleWidthInput(width); + this.handleHeightInput(height); + + } + protected appendDialogContent(container: HTMLDivElement): void { container.className = "container column jc-c g-05"; this.projectNameInput = createTextInput( "project-name-input", "Nome do Projeto", - { min: 0, max: 75, style: { width: 'auto' }, value: "Sem título"}, - this.handleNameInput.bind(this), + { min: 0, max: 75, style: { width: "auto" }, value: "Sem título" }, + (v) => this.handleNameInput(v), ); + this.workAreaWidthInput = createSliderControl( "inp_workarea-width", "Largura", { min: 16, max: 4096, step: 1, value: WORK_AREA_WIDTH }, - this.handleWidthInput.bind(this), - false, + (v) => this.handleWidthInput(v), + true, ); + this.workAreaHeightInput = createSliderControl( - "inp_workarea-width", + "inp_workarea-height", "Altura", { min: 16, max: 4096, step: 1, value: WORK_AREA_HEIGHT }, - this.handleHeightInput.bind(this), - false, + (v) => this.handleHeightInput(v), + true, ); - this.projectNameInput.linkEvents(); - this.workAreaWidthInput.linkEvents(); - this.workAreaHeightInput.linkEvents(); + this.templatesInput = createSelectInput( + "templates-input", + "Templates", + { + optionValues: templates.map((t) => ({ value: t.name, label: t.name })), + value: templates[0]?.name ?? "", + }, + (v) => this.handleTemplateChange(v), + ); container.appendChild(this.projectNameInput.element); + container.appendChild(this.templatesInput.element); container.appendChild(this.workAreaWidthInput.element); container.appendChild(this.workAreaHeightInput.element); + + this.projectNameInput.enable(); + this.workAreaWidthInput.enable(); + this.workAreaHeightInput.enable(); + this.templatesInput.enable(); + + } + protected onOpen(): void { + // Apply initial template after controls are present + enabled. + const initialTemplateName = templates[0]?.name; + if (initialTemplateName) { + this.templatesInput?.setValue(initialTemplateName); + this.handleTemplateChange(initialTemplateName); + } } protected onClose(): void { - this.projectNameInput?.unlinkEvents(); - this.workAreaWidthInput?.unlinkEvents(); - this.workAreaHeightInput?.unlinkEvents(); + this.projectNameInput?.disable(); + this.workAreaWidthInput?.disable(); + this.workAreaHeightInput?.disable(); + this.templatesInput?.disable(); } + protected appendDialogActions(menu: HTMLMenuElement): void { const btnAccept = document.createElement("button"); btnAccept.id = "btn_create-project"; diff --git a/src/components/dialogs/dialog.ts b/src/components/dialogs/dialog.ts index a527f97..ad70a52 100644 --- a/src/components/dialogs/dialog.ts +++ b/src/components/dialogs/dialog.ts @@ -2,76 +2,64 @@ import type { Position } from "../types"; interface IDialogOptions { id: string; - isDraggable?: boolean; - style?: { - minWidth?: string; - }; title: string; + isDraggable?: boolean; + style?: { minWidth?: string }; } export abstract class Dialog { - private dialogEl: HTMLDialogElement | null = null; - private headerEl: HTMLHeadingElement | null = null; - protected dialogContent: HTMLDivElement | null = null; - protected dialogActions: HTMLMenuElement | null = null; + private dialogEl: HTMLDialogElement; + private headerEl: HTMLHeadingElement | null; + protected dialogContent: HTMLDivElement; + protected dialogActions: HTMLMenuElement; private isDragging = false; private dragOffset: Position = { x: 0, y: 0 }; constructor(options: IDialogOptions) { - this.createDOMElements(options); + this.dialogEl = this.createDialogElement(options); + document.body.appendChild(this.dialogEl); + + this.headerEl = this.dialogEl.querySelector(`#dialog-${options.id}-header`); + this.dialogContent = this.dialogEl.querySelector(`#dialog-${options.id}-content`)!; + this.dialogActions = this.dialogEl.querySelector(`#dialog-${options.id}-actions`)!; + if (options.isDraggable) this.enableDrag(); + this.dialogEl.addEventListener("close", () => this.onClose()); } - private createDOMElements(options: IDialogOptions): void { - this.dialogEl = document.createElement("dialog"); - this.dialogEl.id = `dialog-${options.id}`; - this.dialogEl.className = "dialog-common"; - this.dialogEl.style.position = "fixed"; - this.dialogEl.style.minWidth = options?.style?.minWidth || "fit-content"; - if (!options.isDraggable) this.dialogEl.classList.add("fixed-dialog"); - this.resetPosition(); - - this.dialogEl.innerHTML = ` -
-

${options.title}

-
- -
+ private createDialogElement(options: IDialogOptions): HTMLDialogElement { + const dialog = document.createElement("dialog"); + dialog.id = `dialog-${options.id}`; + dialog.className = "dialog-common"; + dialog.style.position = "fixed"; + dialog.style.minWidth = options.style?.minWidth ?? "fit-content"; + if (!options.isDraggable) dialog.classList.add("fixed-dialog"); + this.setCenteredPosition(dialog); + + dialog.innerHTML = ` +
+

+ ${options.title} +

+
+ +
`; - document.body.appendChild(this.dialogEl); - - this.headerEl = this.dialogEl?.querySelector( - `#dialog-${options.id}-header`, - ); - this.dialogContent = this.dialogEl?.querySelector( - `#dialog-${options.id}-content`, - ); - this.dialogActions = this.dialogEl?.querySelector( - `#dialog-${options.id}-actions`, - ); - if (this.dialogContent && this.dialogActions) { - this.appendDialogContent(this.dialogContent); - this.appendDialogActions(this.dialogActions); - } - - this.dialogEl.addEventListener("close", () => { - this.onClose(); - }); + return dialog; } private enableDrag(): void { if (!this.headerEl) return; - this.headerEl.addEventListener("mousedown", (event: MouseEvent) => { - event.preventDefault(); - if (!this.dialogEl) return; + this.headerEl.addEventListener("mousedown", (event) => { + event.preventDefault(); const rect = this.dialogEl.getBoundingClientRect(); + this.dialogEl.style.transform = ""; this.dialogEl.style.left = `${rect.left}px`; this.dialogEl.style.top = `${rect.top}px`; - this.dragOffset.x = event.clientX - rect.left; - this.dragOffset.y = event.clientY - rect.top; + this.dragOffset = { x: event.clientX - rect.left, y: event.clientY - rect.top }; this.isDragging = true; window.addEventListener("mousemove", this.onMouseMove); @@ -80,42 +68,42 @@ export abstract class Dialog { } private onMouseMove = (event: MouseEvent): void => { - if (!this.isDragging || !this.dialogEl) return; + if (!this.isDragging) return; const x = event.clientX - this.dragOffset.x; const y = event.clientY - this.dragOffset.y; - this.dialogEl.style.left = `${x}px`; - this.dialogEl.style.top = `${y}px`; + Object.assign(this.dialogEl.style, { left: `${x}px`, top: `${y}px` }); }; private onMouseUp = (): void => { - if (!this.isDragging) return; this.isDragging = false; - window.removeEventListener("mousemove", this.onMouseMove.bind(this)); - window.removeEventListener("mouseup", this.onMouseUp.bind(this)); + window.removeEventListener("mousemove", this.onMouseMove); + window.removeEventListener("mouseup", this.onMouseUp); }; - private resetPosition(): void { - if (this.dialogEl) { - this.dialogEl.style.top = "50%"; - this.dialogEl.style.left = "50%"; - this.dialogEl.style.transform = "translate(-50%, -50%)"; - } + private setCenteredPosition(dialog: HTMLDialogElement): void { + Object.assign(dialog.style, { + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + }); } public open(): void { + if (!this.dialogContent.hasChildNodes()) { + this.appendDialogContent(this.dialogContent); + this.appendDialogActions(this.dialogActions); + } this.onOpen(); - this.resetPosition(); - this.dialogEl?.showModal(); + this.setCenteredPosition(this.dialogEl); + this.dialogEl.showModal(); } public close(): void { - this.dialogEl?.close(); + this.dialogEl.close(); } protected abstract appendDialogContent(container: HTMLDivElement): void; protected abstract appendDialogActions(menu: HTMLMenuElement): void; - // eslint-disable-next-line @typescript-eslint/no-empty-function - protected onOpen(): void {} - // eslint-disable-next-line @typescript-eslint/no-empty-function - protected onClose(): void {} + protected onOpen(): void {} // hook opcional + protected onClose(): void {} // hook opcional } diff --git a/src/components/gradientMenu.ts b/src/components/gradientMenu.ts index 7c4f2f0..d09ece1 100644 --- a/src/components/gradientMenu.ts +++ b/src/components/gradientMenu.ts @@ -126,9 +126,9 @@ export class GradientMenu { private linkDOMElements = (): void => { if (this.activeGradientElement) { this.currentColorStop = this.activeGradientElement.colorStops[0]; - this.alphaControl?.linkEvents(); + this.alphaControl?.enable(); this.colorControl?.linkEvents(); - this.portionControl?.linkEvents(); + this.portionControl?.enable(); this.gradientFormatSelect = getElementById( "gradient-format-select", ); @@ -147,9 +147,9 @@ export class GradientMenu { private unlinkDOMElements = (): void => { this.activeGradientElement = null; this.currentColorStop = null; - this.alphaControl?.unlinkEvents(); + this.alphaControl?.disable(); this.colorControl?.unlinkEvents(); - this.portionControl?.unlinkEvents(); + this.portionControl?.disable(); this.gradientFormatSelect = getElementById( "gradient-format-select", ); @@ -317,7 +317,7 @@ export class GradientMenu { this.activeGradientElement.colorStops[index].portion = newPortion; this.activeGradientElement.sortColorStops(); this.updateGradientBar(); - this.portionControl?.updateValues(newPortion); + this.portionControl?.setValue(newPortion); this.eventBus.emit("workarea:update"); } }; @@ -341,15 +341,15 @@ export class GradientMenu { index === this.activeGradientElement.colorStops.length - 1; this.portionControl.element.style.display = isFirstOrLast ? "none" : ""; - this.alphaControl.unlinkEvents(); - this.alphaControl.updateValues(this.currentColorStop.alpha); - this.alphaControl.linkEvents(); + this.alphaControl.disable(); + this.alphaControl.setValue(this.currentColorStop.alpha); + this.alphaControl.enable(); this.colorControl.unlinkEvents(); this.colorControl.updateValue(this.currentColorStop.color); this.colorControl.linkEvents(); - this.portionControl.unlinkEvents(); - this.portionControl.updateValues(this.currentColorStop.portion); - this.portionControl.linkEvents(); + this.portionControl.disable(); + this.portionControl.setValue(this.currentColorStop.portion); + this.portionControl.enable(); } } }; diff --git a/src/components/helpers/createSelectInput.ts b/src/components/helpers/createSelectInput.ts index 6371fe7..08d96ec 100644 --- a/src/components/helpers/createSelectInput.ts +++ b/src/components/helpers/createSelectInput.ts @@ -1,18 +1,29 @@ -import type { ISelectOption } from "../types"; - +export interface ISelectOption { + label: string; + value: string; +} export interface ISelectInput { element: HTMLDivElement; - updateValues: (newValue: string) => void; - linkEvents: () => void; - unlinkEvents: () => void; + setValue: (newValue: string) => void; + setOptions?: (newOptions: { + optionValues?: ISelectOption[]; + value?: string; + }) => void; + getValue: () => string; + enable: () => void; + disable: () => void; } export default function createSelectInput( id: string, label: string, - options: { optionValues: Array; value: ISelectOption['value'] }, + options: { + optionValues: Array; + value: ISelectOption["value"]; + }, onChange: (newValue: string) => void, ): ISelectInput { + let currentOptions = { ...options }; const container = document.createElement("div"); container.className = "container ai-c jc-sb g-05"; const labelEl = document.createElement("label"); @@ -24,40 +35,68 @@ export default function createSelectInput( const selectInputEl = document.createElement("select"); selectInputEl.id = `${id}-select-input`; selectInputEl.style.width = "8rem"; - for (const option of options.optionValues) { - const optionEl = document.createElement("option"); - optionEl.value = option.value; - optionEl.innerText = option.label; - selectInputEl.append(optionEl); - } - selectInputEl.value = options.value; - - const updateValues = (newValue: string) => { - selectInputEl.value = newValue; + + const renderOptions = () => { + selectInputEl.innerHTML = ""; + for (const option of currentOptions.optionValues) { + const optionEl = document.createElement("option"); + optionEl.value = option.value; + optionEl.innerText = option.label; + selectInputEl.append(optionEl); + } }; + renderOptions(); + selectInputEl.value = currentOptions.value ?? ""; + + const setValue = (newValue: string) => { + if ([...selectInputEl.options].some((o) => o.value === newValue)) { + selectInputEl.value = newValue; + } else { + selectInputEl.value = selectInputEl.options[0]?.value ?? ""; + } + }; + + const getValue = () => selectInputEl.value; + const handleInputChange = () => { - updateValues(selectInputEl.value); + setValue(selectInputEl.value); onChange(selectInputEl.value); }; - const linkEvents = () => { + let linked = false; + const enable = () => { + if (linked) return; + linked = true; selectInputEl.disabled = false; selectInputEl.addEventListener("change", handleInputChange); }; - const unlinkEvents = () => { + const disable = () => { + if (!linked) return; + linked = false; selectInputEl.disabled = true; - selectInputEl.value = ""; + selectInputEl.value = selectInputEl.options[0]?.value ?? ""; selectInputEl.removeEventListener("change", handleInputChange); }; + const setOptions = (newOptions: { + optionValues?: ISelectOption[]; + value?: string; + }) => { + currentOptions = { ...currentOptions, ...newOptions }; + renderOptions(); + if (newOptions.value !== undefined) setValue(newOptions.value); + }; + container.append(labelEl); container.append(selectInputEl); return { element: container, - updateValues, - linkEvents, - unlinkEvents, + setValue, + setOptions, + getValue, + enable, + disable, }; } diff --git a/src/components/helpers/createSliderControl.ts b/src/components/helpers/createSliderControl.ts index 4c7512b..a82c633 100644 --- a/src/components/helpers/createSliderControl.ts +++ b/src/components/helpers/createSliderControl.ts @@ -1,11 +1,15 @@ import { clamp } from "src/utils/easing"; - export interface ISliderControl { element: HTMLDivElement; - updateValues: (newValue: string | number) => void; - updateOptions: (newOptions: { min?: number; max?: number; step?: number }) => void; - linkEvents: () => void; - unlinkEvents: () => void; + setValue: (newValue: string | number) => void; + setOptions: (newOptions: { + min?: number; + max?: number; + step?: number; + }) => void; + getValue: () => number; + enable: () => void; + disable: () => void; } export default function createSliderControl( @@ -16,9 +20,23 @@ export default function createSliderControl( includeSlider = true, ): ISliderControl { let options = { ...initialOptions }; - const decimalPlaces = options.step.toString().split(".")[1]?.length || 0; + + const getDecimalPlaces = () => { + const parts = options.step.toString().split("."); + return parts[1]?.length ?? 0; + }; + + const snapToStep = (v: number) => { + const step = options.step; + const snapped = Math.round((v - options.min) / step) * step + options.min; + // fix floating precision + const dp = getDecimalPlaces(); + return Number(clamp(Number(snapped.toFixed(dp)), options.min, options.max)); + }; + const clamped = (value: number | string): string => - clamp(Number(value), options.min, options.max).toFixed(decimalPlaces); + snapToStep(Number(value)).toFixed(getDecimalPlaces()); + const container = document.createElement("div"); container.className = "container ai-c jc-sb g-05"; const labelEl = document.createElement("label"); @@ -43,25 +61,39 @@ export default function createSliderControl( inputFieldEl.value = clamped(options.value); inputFieldEl.style.width = "4rem"; - const updateValues = (newValue: string | number) => { - sliderEl.value = clamped(newValue); - inputFieldEl.value = clamped(newValue); + const getValue = () => Number(inputFieldEl.value); + + const setValue = (newValue: string | number) => { + console.log("setvalue", typeof newValue, newValue); + const v = clamped(newValue); + console.log("v", typeof v, v); + sliderEl.value = v; + inputFieldEl.value = v; }; - const updateOptions = (newOptions: { min?: number; max?: number; step?: number }) => { + const setOptions = (newOptions: { + min?: number; + max?: number; + step?: number; + }) => { options = { ...options, ...newOptions }; + // atualizar atributos do input sliderEl.min = options.min.toString(); sliderEl.max = options.max.toString(); + sliderEl.step = options.step.toString(); inputFieldEl.min = options.min.toString(); inputFieldEl.max = options.max.toString(); + inputFieldEl.step = options.step.toString(); + // reaplicar value clamped (mantém coerência com novo range/step) + setValue(inputFieldEl.value); }; const handleSliderChange = () => { - updateValues(sliderEl.value); + setValue(sliderEl.value); onChange(Number(clamped(sliderEl.value))); }; const handleInputChange = () => { - updateValues(inputFieldEl.value); + setValue(inputFieldEl.value); onChange(Number(clamped(inputFieldEl.value))); }; @@ -79,12 +111,15 @@ export default function createSliderControl( }; const handleMouseMove = (evt: MouseEvent) => { - if (isDragging) { - const deltaX = evt.clientX - startX; - const newValue = (startValue + deltaX * options.step).toString(); - updateValues(newValue); - onChange(Number(clamped(newValue))); - } + if (!isDragging) return; + // converte deltaX (px) em deltaValue proporcional à largura do slider + const sliderWidth = sliderEl.clientWidth || 100; // fallback + const deltaX = evt.clientX - startX; + const valueRange = options.max - options.min; + const deltaValue = (deltaX / sliderWidth) * valueRange; + const newValue = snapToStep(startValue + deltaValue); + setValue(newValue); + onChange(newValue); }; const handleMouseUp = (evt: MouseEvent) => { @@ -96,21 +131,32 @@ export default function createSliderControl( } }; - const linkEvents = () => { + let linked = false; + const enable = () => { + if (linked) return; + linked = true; sliderEl.disabled = false; sliderEl.addEventListener("input", handleSliderChange); inputFieldEl.disabled = false; inputFieldEl.addEventListener("change", handleInputChange); labelEl.addEventListener("mousedown", handleMouseDown); }; - const unlinkEvents = () => { + const disable = () => { + if (!linked) return; + linked = false; sliderEl.disabled = true; - sliderEl.value = "0"; sliderEl.removeEventListener("input", handleSliderChange); inputFieldEl.disabled = true; - inputFieldEl.value = "0"; inputFieldEl.removeEventListener("change", handleInputChange); labelEl.removeEventListener("mousedown", handleMouseDown); + // garantir remoção dos listeners de window se estiverem presentes + if (isDragging) { + isDragging = false; + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + } + sliderEl.value = String(options.min) || "0"; + inputFieldEl.value = String(options.min) || "0"; }; container.append(labelEl); @@ -119,9 +165,10 @@ export default function createSliderControl( return { element: container, - updateValues, - updateOptions, - linkEvents, - unlinkEvents, + setValue, + setOptions, + getValue, + enable, + disable, }; } diff --git a/src/components/helpers/createTextInput.ts b/src/components/helpers/createTextInput.ts index 1163d08..cdedb48 100644 --- a/src/components/helpers/createTextInput.ts +++ b/src/components/helpers/createTextInput.ts @@ -1,8 +1,9 @@ export interface ITextInput { element: HTMLDivElement; - updateValues: (newValue: string) => void; - linkEvents: () => void; - unlinkEvents: () => void; + getValue: () => string; + setValue: (newValue: string) => void; + enable: () => void; + disable: () => void; } export default function createTextInput( @@ -31,20 +32,24 @@ export default function createTextInput( inputFieldEl.style.width = options.style?.width ?? "4rem"; options?.value && (inputFieldEl.value = options.value); - const updateValues = (newValue: string) => { + const getValue = (): string => { + return inputFieldEl.value; + }; + + const setValue = (newValue: string) => { inputFieldEl.value = newValue; }; const handleInputChange = () => { - updateValues(inputFieldEl.value); + setValue(inputFieldEl.value); onChange(inputFieldEl.value); }; - const linkEvents = () => { + const enable = () => { inputFieldEl.disabled = false; inputFieldEl.addEventListener("change", handleInputChange); }; - const unlinkEvents = () => { + const disable = () => { inputFieldEl.disabled = true; inputFieldEl.value = ""; inputFieldEl.removeEventListener("change", handleInputChange); @@ -55,8 +60,9 @@ export default function createTextInput( return { element: container, - updateValues, - linkEvents, - unlinkEvents, + getValue, + setValue, + enable, + disable, }; } diff --git a/src/components/textMenu.ts b/src/components/textMenu.ts index 390b2cb..8b83656 100644 --- a/src/components/textMenu.ts +++ b/src/components/textMenu.ts @@ -395,14 +395,14 @@ export class TextMenu { if (this.activeTextElement) { if (this.sizeControl) { - this.sizeControl.unlinkEvents(); - this.sizeControl.updateValues(this.activeTextElement.fontSize); - this.sizeControl.linkEvents(); + this.sizeControl.disable(); + this.sizeControl.setValue(this.activeTextElement.fontSize); + this.sizeControl.enable(); } if (this.lineHeightControl) { - this.lineHeightControl.unlinkEvents(); - this.lineHeightControl.updateValues(this.activeTextElement.lineHeight); - this.lineHeightControl.linkEvents(); + this.lineHeightControl.disable(); + this.lineHeightControl.setValue(this.activeTextElement.lineHeight); + this.lineHeightControl.enable(); } if (this.fillColorControl) { this.fillColorControl.unlinkEvents(); @@ -415,11 +415,11 @@ export class TextMenu { this.strokeColorControl.linkEvents(); } if (this.strokeWidthControl) { - this.strokeWidthControl.unlinkEvents(); - this.strokeWidthControl.updateValues( + this.strokeWidthControl.disable(); + this.strokeWidthControl.setValue( this.activeTextElement.strokeWidth, ); - this.strokeWidthControl.linkEvents(); + this.strokeWidthControl.enable(); } } if (this.isTextSelected) { @@ -461,11 +461,11 @@ export class TextMenu { "click", this.handleDeclineTextChange, ); - this.sizeControl?.unlinkEvents(); - this.lineHeightControl?.unlinkEvents(); + this.sizeControl?.disable(); + this.lineHeightControl?.disable(); this.fillColorControl?.unlinkEvents(); this.strokeColorControl?.unlinkEvents(); - this.strokeWidthControl?.unlinkEvents(); + this.strokeWidthControl?.disable(); } private handleSelectElement(selectedElements: Element[]): void { diff --git a/src/components/transformMenu.ts b/src/components/transformMenu.ts index babd6ef..98b0574 100644 --- a/src/components/transformMenu.ts +++ b/src/components/transformMenu.ts @@ -66,12 +66,12 @@ export class TransformMenu { this.rotationControl && this.opacityControl ) { - this.xPosControl.updateValues(position.x); - this.yPosControl.updateValues(position.y); - this.widthControl.updateValues(size.width); - this.heightControl.updateValues(size.height); - this.rotationControl.updateValues(rotation); - this.opacityControl.updateValues(opacity); + this.xPosControl.setValue(position.x); + this.yPosControl.setValue(position.y); + this.widthControl.setValue(size.width); + this.heightControl.setValue(size.height); + this.rotationControl.setValue(rotation); + this.opacityControl.setValue(opacity); } } @@ -300,27 +300,27 @@ export class TransformMenu { private handleCroppingChanged = (croppingBox: CroppingBox): void => { if (croppingBox) { - this.cropTopControl?.updateValues(croppingBox.top); - this.cropLeftControl?.updateValues(croppingBox.left); - this.cropRightControl?.updateValues(croppingBox.right); - this.cropBottomControl?.updateValues(croppingBox.bottom); + this.cropTopControl?.setValue(croppingBox.top); + this.cropLeftControl?.setValue(croppingBox.left); + this.cropRightControl?.setValue(croppingBox.right); + this.cropBottomControl?.setValue(croppingBox.bottom); } }; private linkDOMElements(): void { const [properties] = this.eventBus.request("transformBox:properties:get"); - this.xPosControl?.linkEvents(); - this.yPosControl?.linkEvents(); - this.widthControl?.linkEvents(); - this.heightControl?.linkEvents(); - this.rotationControl?.linkEvents(); - this.opacityControl?.linkEvents(); - this.xPosControl?.updateValues(properties.position.x); - this.yPosControl?.updateValues(properties.position.y); - this.widthControl?.updateValues(properties.size.width); - this.heightControl?.updateValues(properties.size.height); - this.rotationControl?.updateValues(properties.rotation); - this.opacityControl?.updateValues(properties.opacity); + this.xPosControl?.enable(); + this.yPosControl?.enable(); + this.widthControl?.enable(); + this.heightControl?.enable(); + this.rotationControl?.enable(); + this.opacityControl?.enable(); + this.xPosControl?.setValue(properties.position.x); + this.yPosControl?.setValue(properties.position.y); + this.widthControl?.setValue(properties.size.width); + this.heightControl?.setValue(properties.size.height); + this.rotationControl?.setValue(properties.rotation); + this.opacityControl?.setValue(properties.opacity); const [croppingBox] = this.eventBus.request("transformBox:cropping:get"); @@ -328,41 +328,41 @@ export class TransformMenu { this.cropAccordion.removeAttribute("disabled"); const sizeToUse = properties.unscaledSize || properties.size; - this.cropTopControl?.updateOptions({ max: sizeToUse.height }); - this.cropLeftControl?.updateOptions({ max: sizeToUse.width }); - this.cropRightControl?.updateOptions({ max: sizeToUse.width }); - this.cropBottomControl?.updateOptions({ max: sizeToUse.height }); + this.cropTopControl?.setOptions({ max: sizeToUse.height }); + this.cropLeftControl?.setOptions({ max: sizeToUse.width }); + this.cropRightControl?.setOptions({ max: sizeToUse.width }); + this.cropBottomControl?.setOptions({ max: sizeToUse.height }); - this.cropTopControl?.linkEvents(); - this.cropLeftControl?.linkEvents(); - this.cropRightControl?.linkEvents(); - this.cropBottomControl?.linkEvents(); + this.cropTopControl?.enable(); + this.cropLeftControl?.enable(); + this.cropRightControl?.enable(); + this.cropBottomControl?.enable(); - this.cropTopControl?.updateValues(croppingBox.top); - this.cropLeftControl?.updateValues(croppingBox.left); - this.cropRightControl?.updateValues(croppingBox.right); - this.cropBottomControl?.updateValues(croppingBox.bottom); + this.cropTopControl?.setValue(croppingBox.top); + this.cropLeftControl?.setValue(croppingBox.left); + this.cropRightControl?.setValue(croppingBox.right); + this.cropBottomControl?.setValue(croppingBox.bottom); } else if (this.cropAccordion) { this.cropAccordion.setAttribute("disabled", "true"); this.cropAccordion.open = false; - this.cropTopControl?.unlinkEvents(); - this.cropLeftControl?.unlinkEvents(); - this.cropRightControl?.unlinkEvents(); - this.cropBottomControl?.unlinkEvents(); + this.cropTopControl?.disable(); + this.cropLeftControl?.disable(); + this.cropRightControl?.disable(); + this.cropBottomControl?.disable(); } } private unlinkDOMElements(): void { - this.xPosControl?.unlinkEvents(); - this.yPosControl?.unlinkEvents(); - this.widthControl?.unlinkEvents(); - this.heightControl?.unlinkEvents(); - this.rotationControl?.unlinkEvents(); - this.opacityControl?.unlinkEvents(); - this.cropTopControl?.unlinkEvents(); - this.cropLeftControl?.unlinkEvents(); - this.cropRightControl?.unlinkEvents(); - this.cropBottomControl?.unlinkEvents(); + this.xPosControl?.disable(); + this.yPosControl?.disable(); + this.widthControl?.disable(); + this.heightControl?.disable(); + this.rotationControl?.disable(); + this.opacityControl?.disable(); + this.cropTopControl?.disable(); + this.cropLeftControl?.disable(); + this.cropRightControl?.disable(); + this.cropBottomControl?.disable(); if (this.cropAccordion) { this.cropAccordion.setAttribute("disabled", "true"); this.cropAccordion.open = false; diff --git a/src/components/types.ts b/src/components/types.ts index 5b2cb97..2bed6da 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -121,8 +121,3 @@ export enum TOOL { /** @prop ZOOM - Modificar zoom */ ZOOM = "zoom-tool", } - -export interface ISelectOption { - label: string; - value: string; -} diff --git a/src/filters/colorCorrectionFilter.ts b/src/filters/colorCorrectionFilter.ts index b4b6092..f436fdb 100644 --- a/src/filters/colorCorrectionFilter.ts +++ b/src/filters/colorCorrectionFilter.ts @@ -73,11 +73,11 @@ export class ColorCorrectionFilter extends Filter { { min: 0, max: 200, step: 1, value: properties.saturation as number }, (newValue) => onChange({ saturation: Number(newValue) }), ); - hueControl.linkEvents(); - saturationControl.linkEvents(); - grayScaleControl.linkEvents(); - brightnessControl.linkEvents(); - contrastControl.linkEvents(); + hueControl.enable(); + saturationControl.enable(); + grayScaleControl.enable(); + brightnessControl.enable(); + contrastControl.enable(); container.append( brightnessControl.element, contrastControl.element, diff --git a/src/filters/dropShadowFilter.ts b/src/filters/dropShadowFilter.ts index 7acd3f8..2cf64bf 100644 --- a/src/filters/dropShadowFilter.ts +++ b/src/filters/dropShadowFilter.ts @@ -72,9 +72,9 @@ export class DropShadowFilter extends Filter { { value: properties.color as string }, (newValue) => onChange({ color: newValue }), ); - angleControl.linkEvents(); - distanceControl.linkEvents(); - blurControl.linkEvents(); + angleControl.enable(); + distanceControl.enable(); + blurControl.enable(); colorControl.linkEvents(); container.append( angleControl.element, diff --git a/src/filters/filter.ts b/src/filters/filter.ts index e10f190..3ec9fe3 100644 --- a/src/filters/filter.ts +++ b/src/filters/filter.ts @@ -1,6 +1,6 @@ +import type { ISelectOption } from "src/components/helpers/createSelectInput"; import createSelectInput from "src/components/helpers/createSelectInput"; import createSliderControl from "src/components/helpers/createSliderControl"; -import type { ISelectOption } from "src/components/types"; export type FilterProperty = string | number | undefined; export const COMPOSITE_OPTIONS: Array = [ @@ -86,8 +86,8 @@ export abstract class Filter { (newValue) => onChange({ globalAlpha: Number(newValue) }), ); - compositeControl.linkEvents(); - opacityControl.linkEvents(); + compositeControl.enable(); + opacityControl.enable(); filterControls.append(compositeControl.element); filterControls.append(opacityControl.element); diff --git a/src/filters/outerGlowFilter.ts b/src/filters/outerGlowFilter.ts index 565e0a9..c7ff97c 100644 --- a/src/filters/outerGlowFilter.ts +++ b/src/filters/outerGlowFilter.ts @@ -55,7 +55,7 @@ export class OuterGlowFilter extends Filter { { value: properties.color as string }, (newValue) => onChange({ color: newValue }), ); - blurControl.linkEvents(); + blurControl.enable(); colorControl.linkEvents(); container.append(blurControl.element, colorControl.element); } diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..16874a5 --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,12 @@ +export const templates = [ + { name: "480p (SD widescreen)", width: 854, height: 480 }, + { name: "720p (HD)", width: 1280, height: 720 }, + { name: "900p (HD+)", width: 1600, height: 900 }, + { name: "1080p (Full HD)", width: 1920, height: 1080 }, + { name: "Instagram Post (Square)", width: 1080, height: 1080 }, + { name: "Instagram Post (Portrait)", width: 1080, height: 1350 }, + { name: "Instagram Post (Landscape)", width: 1080, height: 566 }, + { name: "Instagram Story", width: 1080, height: 1920 }, + { name: "Facebook Post", width: 1200, height: 630 }, + { name: "Twitter Post", width: 1024, height: 512 }, +]; From 2a91529b4e966fa5b212c644df9c24e8b212f73b Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Sun, 21 Sep 2025 12:21:44 -0300 Subject: [PATCH 2/6] fix: another dialog properties --- src/components/dialogs/DialogNewProject.ts | 16 +++++++------- src/components/dialogs/dialog.ts | 25 +++++++++++++++------- src/components/helpers/createTextInput.ts | 2 +- src/modals/videoFrameExtractor/index.ts | 3 ++- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/dialogs/DialogNewProject.ts b/src/components/dialogs/DialogNewProject.ts index 60ce33c..1f2b8a3 100644 --- a/src/components/dialogs/DialogNewProject.ts +++ b/src/components/dialogs/DialogNewProject.ts @@ -91,7 +91,7 @@ export class DialogNewProject extends Dialog { "Largura", { min: 16, max: 4096, step: 1, value: WORK_AREA_WIDTH }, (v) => this.handleWidthInput(v), - true, + false, ); this.workAreaHeightInput = createSliderControl( @@ -99,7 +99,7 @@ export class DialogNewProject extends Dialog { "Altura", { min: 16, max: 4096, step: 1, value: WORK_AREA_HEIGHT }, (v) => this.handleHeightInput(v), - true, + false, ); this.templatesInput = createSelectInput( @@ -116,14 +116,14 @@ export class DialogNewProject extends Dialog { container.appendChild(this.templatesInput.element); container.appendChild(this.workAreaWidthInput.element); container.appendChild(this.workAreaHeightInput.element); - - this.projectNameInput.enable(); - this.workAreaWidthInput.enable(); - this.workAreaHeightInput.enable(); - this.templatesInput.enable(); - } + protected onOpen(): void { + this.projectNameInput?.enable(); + this.workAreaWidthInput?.enable(); + this.workAreaHeightInput?.enable(); + this.templatesInput?.enable(); + // Apply initial template after controls are present + enabled. const initialTemplateName = templates[0]?.name; if (initialTemplateName) { diff --git a/src/components/dialogs/dialog.ts b/src/components/dialogs/dialog.ts index ad70a52..b1bb396 100644 --- a/src/components/dialogs/dialog.ts +++ b/src/components/dialogs/dialog.ts @@ -10,8 +10,8 @@ interface IDialogOptions { export abstract class Dialog { private dialogEl: HTMLDialogElement; private headerEl: HTMLHeadingElement | null; - protected dialogContent: HTMLDivElement; - protected dialogActions: HTMLMenuElement; + protected dialogContent: HTMLDivElement | null; + protected dialogActions: HTMLMenuElement | null; private isDragging = false; private dragOffset: Position = { x: 0, y: 0 }; @@ -20,8 +20,12 @@ export abstract class Dialog { document.body.appendChild(this.dialogEl); this.headerEl = this.dialogEl.querySelector(`#dialog-${options.id}-header`); - this.dialogContent = this.dialogEl.querySelector(`#dialog-${options.id}-content`)!; - this.dialogActions = this.dialogEl.querySelector(`#dialog-${options.id}-actions`)!; + this.dialogContent = this.dialogEl.querySelector( + `#dialog-${options.id}-content`, + ); + this.dialogActions = this.dialogEl.querySelector( + `#dialog-${options.id}-actions`, + ); if (options.isDraggable) this.enableDrag(); this.dialogEl.addEventListener("close", () => this.onClose()); @@ -59,7 +63,10 @@ export abstract class Dialog { this.dialogEl.style.left = `${rect.left}px`; this.dialogEl.style.top = `${rect.top}px`; - this.dragOffset = { x: event.clientX - rect.left, y: event.clientY - rect.top }; + this.dragOffset = { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; this.isDragging = true; window.addEventListener("mousemove", this.onMouseMove); @@ -89,7 +96,7 @@ export abstract class Dialog { } public open(): void { - if (!this.dialogContent.hasChildNodes()) { + if (this.dialogContent && this.dialogActions && !this.dialogContent.hasChildNodes()) { this.appendDialogContent(this.dialogContent); this.appendDialogActions(this.dialogActions); } @@ -104,6 +111,8 @@ export abstract class Dialog { protected abstract appendDialogContent(container: HTMLDivElement): void; protected abstract appendDialogActions(menu: HTMLMenuElement): void; - protected onOpen(): void {} // hook opcional - protected onClose(): void {} // hook opcional + // eslint-disable-next-line @typescript-eslint/no-empty-function + protected onOpen(): void {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + protected onClose(): void {} } diff --git a/src/components/helpers/createTextInput.ts b/src/components/helpers/createTextInput.ts index cdedb48..3e7d7be 100644 --- a/src/components/helpers/createTextInput.ts +++ b/src/components/helpers/createTextInput.ts @@ -51,7 +51,7 @@ export default function createTextInput( }; const disable = () => { inputFieldEl.disabled = true; - inputFieldEl.value = ""; + inputFieldEl.value = options.value || ""; inputFieldEl.removeEventListener("change", handleInputChange); }; diff --git a/src/modals/videoFrameExtractor/index.ts b/src/modals/videoFrameExtractor/index.ts index 5bdaf8b..2184a2f 100644 --- a/src/modals/videoFrameExtractor/index.ts +++ b/src/modals/videoFrameExtractor/index.ts @@ -362,7 +362,8 @@ export class VideoFrameExtractor { * can be sent to the WorkArea as a new element or copied to clipboard. * @param {boolean} toClipboard - if true, copies extracted area to clipboard instead. */ - private extractFrame(toClipboard = false): void { + // eslint-disable-next-line @typescript-eslint/no-inferrable-types + private extractFrame(toClipboard: boolean = false): void { if (this.offScreen && this.preview && this.extractBox) { const { width: previewWidth, height: previewHeight } = this.preview.canvas; From 91c27824fddb7719540502da760ff2e8d6118107 Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Sun, 21 Sep 2025 16:32:15 -0300 Subject: [PATCH 3/6] feat: new templates for thumbnails --- src/components/dialogs/DialogNewProject.ts | 76 ++++++++++++++------- src/components/helpers/createSelectInput.ts | 11 ++- src/constants.ts | 2 + src/templates.ts | 12 ---- 4 files changed, 64 insertions(+), 37 deletions(-) delete mode 100644 src/templates.ts diff --git a/src/components/dialogs/DialogNewProject.ts b/src/components/dialogs/DialogNewProject.ts index 1f2b8a3..15e949d 100644 --- a/src/components/dialogs/DialogNewProject.ts +++ b/src/components/dialogs/DialogNewProject.ts @@ -5,13 +5,48 @@ import type { ISliderControl } from "../helpers/createSliderControl"; import createSliderControl from "../helpers/createSliderControl"; import type { ITextInput } from "../helpers/createTextInput"; import createTextInput from "../helpers/createTextInput"; -import type { ISelectInput } from "../helpers/createSelectInput"; +import type { ISelectInput, ISelectOption } from "../helpers/createSelectInput"; import createSelectInput from "../helpers/createSelectInput"; -import { templates } from "src/templates"; +import { OPTION_SEPARATOR_VALUE } from "src/constants"; const WORK_AREA_WIDTH = 1920; const WORK_AREA_HEIGHT = 1080; +// Keys for the template data +const T_480P_WIDE = "480p (SD wide)"; +const T_720P_HD = "720p (HD)"; +const T_900P_HD_PLUS = "900p (HD+)"; +const T_1080P_FULL_HD = "Youtube Miniatura (Full HD)"; +const T_INSTA_SQUARE = "Instagram Post (Quadrado)"; +const T_INSTA_PORTRAIT = "Instagram Post (Retrato)"; +const T_INSTA_LANDSCAPE = "Instagram Post (Paisagem)"; +const T_FACEBOOK_POST = "Facebook Post"; + +const TEMPLATE_DATA: Record = { + [T_480P_WIDE]: { width: 854, height: 480 }, + [T_720P_HD]: { width: 1280, height: 720 }, + [T_900P_HD_PLUS]: { width: 1600, height: 900 }, + [T_1080P_FULL_HD]: { width: 1920, height: 1080 }, + [T_INSTA_SQUARE]: { width: 1080, height: 1080 }, + [T_INSTA_PORTRAIT]: { width: 1080, height: 1350 }, + [T_INSTA_LANDSCAPE]: { width: 1080, height: 566 }, + [T_FACEBOOK_POST]: { width: 1200, height: 630 }, +}; + +const TEMPLATE_OPTIONS: Array = [ + { label: "Personalizado", value: "CUSTOM" }, + { label: "--- Padrões de Vídeo ---", value: OPTION_SEPARATOR_VALUE }, + { label: T_480P_WIDE, value: T_480P_WIDE }, + { label: T_720P_HD, value: T_720P_HD }, + { label: T_900P_HD_PLUS, value: T_900P_HD_PLUS }, + { label: T_1080P_FULL_HD, value: T_1080P_FULL_HD }, + { label: "--- Redes Sociais ---", value: OPTION_SEPARATOR_VALUE }, + { label: T_INSTA_SQUARE, value: T_INSTA_SQUARE }, + { label: T_INSTA_PORTRAIT, value: T_INSTA_PORTRAIT }, + { label: T_INSTA_LANDSCAPE, value: T_INSTA_LANDSCAPE }, + { label: T_FACEBOOK_POST, value: T_FACEBOOK_POST }, +]; + export class DialogNewProject extends Dialog { private eventBus: EventBus; private projectData: IProjectData | null = null; @@ -45,35 +80,28 @@ export class DialogNewProject extends Dialog { } private handleWidthInput(newValue: number): void { - if (this.projectData) { + if (this.projectData && this.templatesInput) { this.projectData.workAreaSize.width = newValue; + this.templatesInput.setValue("CUSTOM"); } } private handleHeightInput(newValue: number): void { - if (this.projectData) { + if (this.projectData && this.templatesInput) { this.projectData.workAreaSize.height = newValue; + this.templatesInput.setValue("CUSTOM"); } } private handleTemplateChange(newValue: string): void { - if (!this.workAreaWidthInput || !this.workAreaHeightInput) { - console.log("[DialogNewProject] ignorando mudança de template, sliders ainda não criados"); - return; - } - const tpl = templates.find((t) => t.name === newValue); - if (!tpl) { - console.warn("[DialogNewProject] Template não encontrado:", newValue); - return; - } - + const tpl = TEMPLATE_DATA[newValue]; + if (!tpl || !this.projectData) return; const width = tpl.width; const height = tpl.height; this.workAreaWidthInput?.setValue(width); this.workAreaHeightInput?.setValue(height); - this.handleWidthInput(width); - this.handleHeightInput(height); - + this.projectData.workAreaSize.width = width; + this.projectData.workAreaSize.height = height; } protected appendDialogContent(container: HTMLDivElement): void { @@ -106,8 +134,8 @@ export class DialogNewProject extends Dialog { "templates-input", "Templates", { - optionValues: templates.map((t) => ({ value: t.name, label: t.name })), - value: templates[0]?.name ?? "", + optionValues: TEMPLATE_OPTIONS, + value: TEMPLATE_OPTIONS[0].value, }, (v) => this.handleTemplateChange(v), ); @@ -125,10 +153,12 @@ export class DialogNewProject extends Dialog { this.templatesInput?.enable(); // Apply initial template after controls are present + enabled. - const initialTemplateName = templates[0]?.name; - if (initialTemplateName) { - this.templatesInput?.setValue(initialTemplateName); - this.handleTemplateChange(initialTemplateName); + const initialTemplate = TEMPLATE_OPTIONS.find( + (opt) => opt.value !== OPTION_SEPARATOR_VALUE, + ); + if (initialTemplate) { + this.templatesInput?.setValue(initialTemplate.value); + this.handleTemplateChange(initialTemplate.value); } } diff --git a/src/components/helpers/createSelectInput.ts b/src/components/helpers/createSelectInput.ts index 08d96ec..2fc0f2f 100644 --- a/src/components/helpers/createSelectInput.ts +++ b/src/components/helpers/createSelectInput.ts @@ -1,3 +1,5 @@ +import { OPTION_SEPARATOR_VALUE } from "src/constants"; + export interface ISelectOption { label: string; value: string; @@ -40,8 +42,13 @@ export default function createSelectInput( selectInputEl.innerHTML = ""; for (const option of currentOptions.optionValues) { const optionEl = document.createElement("option"); - optionEl.value = option.value; - optionEl.innerText = option.label; + if (option.value === OPTION_SEPARATOR_VALUE) { + optionEl.disabled = true; + optionEl.innerText = option.label; + } else { + optionEl.value = option.value; + optionEl.innerText = option.label; + } selectInputEl.append(optionEl); } }; diff --git a/src/constants.ts b/src/constants.ts index 73e839a..3c903ae 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,4 @@ export const TOOL_MENU_WIDTH = 40; export const SIDE_MENU_WIDTH = 320; + +export const OPTION_SEPARATOR_VALUE = 'OPTION_SEPARATOR_VALUE'; diff --git a/src/templates.ts b/src/templates.ts deleted file mode 100644 index 16874a5..0000000 --- a/src/templates.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const templates = [ - { name: "480p (SD widescreen)", width: 854, height: 480 }, - { name: "720p (HD)", width: 1280, height: 720 }, - { name: "900p (HD+)", width: 1600, height: 900 }, - { name: "1080p (Full HD)", width: 1920, height: 1080 }, - { name: "Instagram Post (Square)", width: 1080, height: 1080 }, - { name: "Instagram Post (Portrait)", width: 1080, height: 1350 }, - { name: "Instagram Post (Landscape)", width: 1080, height: 566 }, - { name: "Instagram Story", width: 1080, height: 1920 }, - { name: "Facebook Post", width: 1200, height: 630 }, - { name: "Twitter Post", width: 1024, height: 512 }, -]; From bbc8fc6c5d825e6f456ad7cd5cbfd68d8c729cc0 Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Mon, 22 Sep 2025 15:42:04 -0300 Subject: [PATCH 4/6] fix: changed style, removed logs --- src/components/dialogs/DialogNewProject.ts | 3 +++ src/components/helpers/createSelectInput.ts | 3 ++- src/components/helpers/createSliderControl.ts | 2 -- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/dialogs/DialogNewProject.ts b/src/components/dialogs/DialogNewProject.ts index 15e949d..52c0a81 100644 --- a/src/components/dialogs/DialogNewProject.ts +++ b/src/components/dialogs/DialogNewProject.ts @@ -135,6 +135,9 @@ export class DialogNewProject extends Dialog { "Templates", { optionValues: TEMPLATE_OPTIONS, + style: { + width: 'auto', + }, value: TEMPLATE_OPTIONS[0].value, }, (v) => this.handleTemplateChange(v), diff --git a/src/components/helpers/createSelectInput.ts b/src/components/helpers/createSelectInput.ts index 2fc0f2f..3b8969a 100644 --- a/src/components/helpers/createSelectInput.ts +++ b/src/components/helpers/createSelectInput.ts @@ -22,6 +22,7 @@ export default function createSelectInput( options: { optionValues: Array; value: ISelectOption["value"]; + style?: { width: string }; }, onChange: (newValue: string) => void, ): ISelectInput { @@ -36,7 +37,7 @@ export default function createSelectInput( const selectInputEl = document.createElement("select"); selectInputEl.id = `${id}-select-input`; - selectInputEl.style.width = "8rem"; + selectInputEl.style.width = options.style?.width || "8rem"; const renderOptions = () => { selectInputEl.innerHTML = ""; diff --git a/src/components/helpers/createSliderControl.ts b/src/components/helpers/createSliderControl.ts index a82c633..7c456e7 100644 --- a/src/components/helpers/createSliderControl.ts +++ b/src/components/helpers/createSliderControl.ts @@ -64,9 +64,7 @@ export default function createSliderControl( const getValue = () => Number(inputFieldEl.value); const setValue = (newValue: string | number) => { - console.log("setvalue", typeof newValue, newValue); const v = clamped(newValue); - console.log("v", typeof v, v); sliderEl.value = v; inputFieldEl.value = v; }; From 36f99414155e32f601056facfdcd2c587bcf2cae Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Mon, 22 Sep 2025 21:20:46 -0300 Subject: [PATCH 5/6] feat: add aspect ratio selector for extractbox --- src/components/extractBox/extractBox.ts | 310 +++++++++++++++++++----- src/modals/videoFrameExtractor/index.ts | 56 ++++- 2 files changed, 299 insertions(+), 67 deletions(-) diff --git a/src/components/extractBox/extractBox.ts b/src/components/extractBox/extractBox.ts index 859d1f3..59fbd20 100644 --- a/src/components/extractBox/extractBox.ts +++ b/src/components/extractBox/extractBox.ts @@ -4,11 +4,17 @@ import type { Position, Size, TBoundingBox } from "../types"; const LINE_WIDTH = 4; const CENTER_RADIUS = 6; -const FRAME_RATIO: Record = { - vertical_wide: 1.77, - horizontal_wide: 0.5625, - letterbox: 1.33, -} as const; + +export type ExtractBoxHandleKeys = + | "BOTTOM" + | "BOTTOM_LEFT" + | "BOTTOM_RIGHT" + | "CENTER" + | "LEFT" + | "RIGHT" + | "TOP" + | "TOP_LEFT" + | "TOP_RIGHT"; export class ExtractBox { private position: Position = { x: 0, y: 0 }; @@ -18,13 +24,46 @@ export class ExtractBox { private lastMousePosition: Position | null = { x: 0, y: 0 }; private eventBus: EventBus; + public handles: Record | null = null; + public hoveredHandle: ExtractBoxHandleKeys | null = null; + public selectedHandle: ExtractBoxHandleKeys | null = null; + private aspectRatio: number | null = null; + constructor(canvas: HTMLCanvasElement, eventBus: EventBus) { this.canvas = canvas; this.eventBus = eventBus; this.setupInitialBox(); + this.generateHandles(); this.draw(); } + public setAspectRatio(ratioString: string): void { + if (ratioString === "custom") { + this.aspectRatio = null; + return; + } + + const [width, height] = ratioString.split(":").map(Number); + this.aspectRatio = width / height; + + let newWidth = this.canvas.width; + let newHeight = newWidth / this.aspectRatio; + + if (newHeight > this.canvas.height) { + newHeight = this.canvas.height; + newWidth = newHeight * this.aspectRatio; + } + + this.size = { width: newWidth, height: newHeight }; + this.position = { + x: (this.canvas.width - newWidth) / 2, + y: (this.canvas.height - newHeight) / 2, + }; + + this.generateHandles(); + this.eventBus.emit("vfe:update"); + } + public getBoundingBox(): TBoundingBox { return { x1: this.position.x, @@ -34,37 +73,104 @@ export class ExtractBox { }; } - public onClick(evt: MouseEvent): void { - const { offsetX: x, offsetY: y } = evt; - const centerPosition = this.getCenter(); - if ( - x > centerPosition.x - CENTER_RADIUS && - x < centerPosition.x + CENTER_RADIUS && - y > centerPosition.y - CENTER_RADIUS && - y < centerPosition.y + CENTER_RADIUS - ) { - this.isDragging = true; - this.lastMousePosition = { x, y }; - const onMouseMove = (evt: MouseEvent): void => this.onMouseMove(evt); - const onMouseUp = (): void => { - this.isDragging = false; - this.lastMousePosition = null; - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", onMouseUp); - }; - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("mouseup", onMouseUp); - } + public onMouseDown(evt: MouseEvent): void { + this.hoverHandle(evt); + this.selectHandle(); + + if (!this.selectedHandle) return; + + this.isDragging = true; + const canvasRect = this.canvas.getBoundingClientRect(); + this.lastMousePosition = { + x: evt.clientX - canvasRect.left, + y: evt.clientY - canvasRect.top, + }; + + const onMouseMove = (e: MouseEvent): void => this.onMouseMove(e); + const onMouseUp = (): void => { + this.isDragging = false; + this.lastMousePosition = null; + this.selectedHandle = null; + this.hoveredHandle = null; + this.eventBus.emit("vfe:update"); + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); } private onMouseMove(evt: MouseEvent): void { - if (this.isDragging && this.lastMousePosition) { - const dX = evt.offsetX - this.lastMousePosition.x; - const dY = evt.offsetY - this.lastMousePosition.y; + if (!this.isDragging || !this.lastMousePosition) return; + + const canvasRect = this.canvas.getBoundingClientRect(); + const mouseX = evt.clientX - canvasRect.left; + const mouseY = evt.clientY - canvasRect.top; + + const dX = mouseX - this.lastMousePosition.x; + const dY = mouseY - this.lastMousePosition.y; + + if (this.selectedHandle === "CENTER") { this.moveExtractBox(dX, dY); - this.lastMousePosition = { x: evt.offsetX, y: evt.offsetY }; - this.eventBus.emit("vfe:update"); + } else if (this.selectedHandle) { + this.resizeExtractBox(dX, dY); } + + this.lastMousePosition = { x: mouseX, y: mouseY }; + this.eventBus.emit("vfe:update"); + } + + private resizeExtractBox(deltaX: number, deltaY: number): void { + const { xSign, ySign } = this.calculateSignAndAnchor(); + + let newWidth = this.size.width + deltaX * xSign; + let newHeight = this.size.height + deltaY * ySign; + + if (this.aspectRatio) { + if (xSign !== 0 && ySign !== 0) { + // Corner handles: maintain aspect ratio based on the larger delta direction + if (Math.abs(deltaX) > Math.abs(deltaY)) { + newHeight = newWidth / this.aspectRatio; + } else { + newWidth = newHeight * this.aspectRatio; + } + } else if (xSign !== 0) { + // Horizontal handles + newHeight = newWidth / this.aspectRatio; + } else if (ySign !== 0) { + // Vertical handles + newWidth = newHeight * this.aspectRatio; + } + } + + const minSize = 10; + if (newWidth < minSize) { + newWidth = minSize; + if (this.aspectRatio) newHeight = newWidth / this.aspectRatio; + } + if (newHeight < minSize) { + newHeight = minSize; + if (this.aspectRatio) newWidth = newHeight * this.aspectRatio; + } + + if (xSign < 0) { + this.position.x -= newWidth - this.size.width; + } + if (ySign < 0) { + this.position.y -= newHeight - this.size.height; + } + + this.size.width = newWidth; + this.size.height = newHeight; + + // Clamp to canvas boundaries + this.position.x = clamp(this.position.x, 0, this.canvas.width - this.size.width); + this.position.y = clamp(this.position.y, 0, this.canvas.height - this.size.height); + this.size.width = clamp(this.size.width, minSize, this.canvas.width - this.position.x); + this.size.height = clamp(this.size.height, minSize, this.canvas.height - this.position.y); + + this.generateHandles(); } private moveExtractBox(deltaX: number, deltaY: number): void { @@ -82,24 +188,14 @@ export class ExtractBox { 0, this.canvas.height - this.size.height, ); + this.generateHandles(); } private setupInitialBox(): void { - const ratio = FRAME_RATIO.horizontal_wide; - let width = this.canvas.width; - let height = Math.ceil(this.canvas.width * ratio); - if (height > this.canvas.height) { - height = this.canvas.height; - width = Math.ceil(this.canvas.height / ratio); - } - this.position = { - x: this.canvas.width * 0.5 - width * 0.5, - y: this.canvas.height * 0.5 - height * 0.5, - }; - this.size = { height, width }; + this.size = { width: this.canvas.width, height: this.canvas.height }; + this.position = { x: 0, y: 0 }; } - /** Returns the center of the transform box */ public getCenter(): Position { return { x: this.position.x + this.size.width * 0.5, @@ -107,10 +203,28 @@ export class ExtractBox { }; } + private generateHandles(): void { + const { x, y } = this.position; + const { width, height } = this.size; + const halfWidth = width / 2; + const halfHeight = height / 2; + + this.handles = { + TOP_LEFT: { x, y }, + TOP: { x: x + halfWidth, y }, + TOP_RIGHT: { x: x + width, y }, + RIGHT: { x: x + width, y: y + halfHeight }, + BOTTOM_RIGHT: { x: x + width, y: y + height }, + BOTTOM: { x: x + halfWidth, y: y + height }, + BOTTOM_LEFT: { x, y: y + height }, + LEFT: { x, y: y + halfHeight }, + CENTER: { x: x + halfWidth, y: y + halfHeight }, + }; + } + public draw(): void { const context = this.canvas.getContext("2d"); if (!context) return; - const centerPosition = this.getCenter(); // Draw extracting box context.save(); @@ -123,18 +237,100 @@ export class ExtractBox { this.size.height - LINE_WIDTH, ); - // Draw centerHandle - context.fillStyle = "green"; - context.beginPath(); - context.arc( - centerPosition.x, - centerPosition.y, - CENTER_RADIUS, - 0, - Math.PI * 2, - ); - context.fill(); - context.closePath(); + // Draw handles + if (this.handles) { + for (const key of Object.keys(this.handles) as ExtractBoxHandleKeys[]) { + const point = this.handles[key]; + context.fillStyle = key === this.hoveredHandle ? "yellow" : "green"; + context.beginPath(); + context.arc(point.x, point.y, CENTER_RADIUS, 0, Math.PI * 2); + context.fill(); + context.closePath(); + } + } context.restore(); } + + public hoverHandle(evt: MouseEvent): void { + if (this.handles) { + const { offsetX, offsetY } = evt; + const hitHandle = ( + Object.keys(this.handles) as ExtractBoxHandleKeys[] + ).find((key) => { + if (this.handles) { + const point = this.handles[key]; + return Math.hypot(offsetX - point.x, offsetY - point.y) < 10; + } + return false; + }); + this.hoveredHandle = hitHandle || null; + this.eventBus.emit("vfe:update"); + } + } + + private selectHandle(): boolean { + this.selectedHandle = this.hoveredHandle; + return !!this.hoveredHandle; + } + + private calculateSignAndAnchor(): { + anchor: Position; + xSign: 1 | 0 | -1; + ySign: 1 | 0 | -1; + } { + let anchor: Position = { x: 0, y: 0 }; + let xSign: 1 | 0 | -1 = 1; + let ySign: 1 | 0 | -1 = 1; + + if (!this.handles || !this.selectedHandle) return { anchor, xSign, ySign }; + + switch (this.selectedHandle) { + case "TOP_LEFT": + xSign = -1; + ySign = -1; + anchor = this.handles.BOTTOM_RIGHT; + break; + case "TOP_RIGHT": + xSign = 1; + ySign = -1; + anchor = this.handles.BOTTOM_LEFT; + break; + case "BOTTOM_RIGHT": + xSign = 1; + ySign = 1; + anchor = this.handles.TOP_LEFT; + break; + case "BOTTOM_LEFT": + xSign = -1; + ySign = 1; + anchor = this.handles.TOP_RIGHT; + break; + case "TOP": + xSign = 0; + ySign = -1; + anchor = this.handles.BOTTOM; + break; + case "RIGHT": + xSign = 1; + ySign = 0; + anchor = this.handles.LEFT; + break; + case "BOTTOM": + xSign = 0; + ySign = 1; + anchor = this.handles.TOP; + break; + case "LEFT": + xSign = -1; + ySign = 0; + anchor = this.handles.RIGHT; + break; + case "CENTER": + xSign = 0; + ySign = 0; + anchor = this.handles.CENTER; + break; + } + return { anchor, xSign, ySign }; + } } diff --git a/src/modals/videoFrameExtractor/index.ts b/src/modals/videoFrameExtractor/index.ts index 2184a2f..4cfef6e 100644 --- a/src/modals/videoFrameExtractor/index.ts +++ b/src/modals/videoFrameExtractor/index.ts @@ -1,5 +1,7 @@ import "../../assets/main.css"; import { Alerts } from "src/components/alerts/alerts"; +import type { ISelectInput } from "src/components/helpers/createSelectInput"; +import createSelectInput from "src/components/helpers/createSelectInput"; import { clamp } from "src/utils/easing"; import { EventBus } from "src/utils/eventBus"; import formatFrameIntoTime from "src/utils/formatFrameIntoTime"; @@ -21,8 +23,8 @@ export class VideoFrameExtractor { context: CanvasRenderingContext2D; } | null = null; private offScreen: { - canvas: OffscreenCanvas; - context: OffscreenCanvasRenderingContext2D; + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; } | null = null; private videoMetadata: (IVideoMetadata & { videoRatio?: number }) | null = null; @@ -30,6 +32,7 @@ export class VideoFrameExtractor { private copyToClipBoardBtn: HTMLButtonElement | null = null; private videoDurationSlider: HTMLInputElement | null = null; private videoDurationIndicator: HTMLDivElement | null = null; + private aspectRatioSelect: ISelectInput | null = null; private extractBox: ExtractBox | null = null; private currentThumbIndex = -1; private rafId: number | null = null; @@ -44,9 +47,33 @@ export class VideoFrameExtractor { } private createDOMElements(): void { + const mainContainer = getElementById("vfe_main-container"); const previewCanvas = getElementById("video-canvas"); + this.aspectRatioSelect = createSelectInput( + "vfe_aspect-ratio", + "Proporção", + { + optionValues: [ + { label: "Personalizado", value: "custom" }, + { label: "1:1 (Quadrado)", value: "1:1" }, + { label: "4:3 (Tradicional)", value: "4:3" }, + { label: "5:7 (Retrato)", value: "5:7" }, + { label: "16:9 (Widescreen)", value: "16:9" }, + { label: "21:9 (Cinemascópio)", value: "21:9" }, + ], + value: "16:9", + }, + (value) => { + if (this.extractBox) { + this.extractBox.setAspectRatio(value); + } + }, + ); + this.aspectRatioSelect.enable(); + mainContainer.insertBefore(this.aspectRatioSelect.element, previewCanvas); + const extractCanvas = document.createElement("canvas"); - const offScreenCanvas = new OffscreenCanvas(0, 0); + const offScreenCanvas = document.createElement("canvas"); const previewContext = previewCanvas.getContext("2d"); const extractContext = extractCanvas.getContext("2d"); @@ -84,7 +111,8 @@ export class VideoFrameExtractor { ); window.addEventListener("keydown", this.handleKeyDown.bind(this)); - const seekButtons = document.querySelectorAll("[data-seek]"); + const seekButtons = + document.querySelectorAll("[data-seek]"); seekButtons.forEach((button) => { button.addEventListener("click", this.handleSeekButton.bind(this)); }); @@ -93,7 +121,12 @@ export class VideoFrameExtractor { if (this.preview) { this.preview.canvas.addEventListener("mousedown", (evt) => { if (this.extractBox) { - return this.extractBox.onClick(evt); + return this.extractBox.onMouseDown(evt); + } + }); + this.preview.canvas.addEventListener("mousemove", (evt) => { + if (this.extractBox) { + return this.extractBox.hoverHandle(evt); } }); } @@ -120,6 +153,9 @@ export class VideoFrameExtractor { this.preview.canvas.width = canvasWidth; this.preview.canvas.height = canvasHeight; this.extractBox = new ExtractBox(this.preview.canvas, this.eventBus); + if (this.aspectRatioSelect) { + this.extractBox.setAspectRatio(this.aspectRatioSelect.getValue()); + } this.extractFrameBtn.onclick = (): void => { if (this.extractBox) { this.extractFrame(); @@ -406,11 +442,11 @@ export class VideoFrameExtractor { }, "image/png"); } else { const imageUrl = this.extract.canvas.toDataURL("image/png"); - this.eventBus.emit("alert:add", { - message: "Quadro do vídeo extraído para o Projeto", - title: "Quadro Extraído", - type: "success", - }); + this.eventBus.emit("alert:add", { + message: "Quadro do vídeo extraído para o Projeto", + title: "Quadro Extraído", + type: "success", + }); window.api.sendFrameToWorkArea(imageUrl); } } From e81e168d388ed8fab3cab07e0cc909cbdaf14554 Mon Sep 17 00:00:00 2001 From: Pesterenan Date: Mon, 22 Sep 2025 21:32:11 -0300 Subject: [PATCH 6/6] tests: updating method calls on tests --- src/components/gradientMenu.test.ts | 15 +++++---- src/components/textMenu.test.ts | 14 +++++---- src/components/transformMenu.test.ts | 47 ++++++++++++++-------------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/components/gradientMenu.test.ts b/src/components/gradientMenu.test.ts index aaea4ac..fab9bd8 100644 --- a/src/components/gradientMenu.test.ts +++ b/src/components/gradientMenu.test.ts @@ -9,9 +9,10 @@ import type { TElementData } from "./types"; jest.mock("./helpers/createSliderControl", () => { return jest.fn(() => ({ element: document.createElement("div"), - updateValues: jest.fn(), - linkEvents: jest.fn(), - unlinkEvents: jest.fn(), + setValue: jest.fn(), + getValue: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), })); }); @@ -38,12 +39,14 @@ describe("GradientMenu", () => { handleFunctions[id] = callback; return { element: document.createElement("div"), - updateValues: jest.fn(), - linkEvents: jest.fn(), - unlinkEvents: jest.fn(), + setValue: jest.fn(), + getValue: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), }; }, ); + (createColorControl as jest.Mock).mockImplementation( (id, _label, _options, callback) => { handleFunctions[id] = callback; diff --git a/src/components/textMenu.test.ts b/src/components/textMenu.test.ts index 1f28279..28f7274 100644 --- a/src/components/textMenu.test.ts +++ b/src/components/textMenu.test.ts @@ -9,9 +9,10 @@ import type { TElementData } from "./types"; jest.mock("./helpers/createSliderControl", () => { return jest.fn(() => ({ element: document.createElement("div"), - updateValues: jest.fn(), - linkEvents: jest.fn(), - unlinkEvents: jest.fn(), + setValue: jest.fn(), + getValue: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), })); }); @@ -38,9 +39,10 @@ describe("TextMenu", () => { handleFunctions[id] = callback; return { element: document.createElement("div"), - updateValues: jest.fn(), - linkEvents: jest.fn(), - unlinkEvents: jest.fn(), + setValue: jest.fn(), + getValue: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), }; }, ); diff --git a/src/components/transformMenu.test.ts b/src/components/transformMenu.test.ts index 1d21249..3f09f59 100644 --- a/src/components/transformMenu.test.ts +++ b/src/components/transformMenu.test.ts @@ -6,10 +6,11 @@ import type { TElementData } from "./types"; const mockSliderControl = { element: document.createElement("div"), - updateValues: jest.fn(), - updateOptions: jest.fn(), - linkEvents: jest.fn(), - unlinkEvents: jest.fn(), + setValue: jest.fn(), + setOptions: jest.fn(), + getValue: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), }; jest.mock("./helpers/createSliderControl", () => { @@ -89,23 +90,23 @@ describe("TransformMenu", () => { eventBus.emit("selection:changed", { selectedElements: [mockElement] }); - expect(mockSliderControl.linkEvents).toHaveBeenCalledTimes(10); - expect(mockSliderControl.updateValues).toHaveBeenCalledTimes(10); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(10); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(20); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(100); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(200); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(45); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(0.8); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(1); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(2); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(3); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(4); + expect(mockSliderControl.enable).toHaveBeenCalledTimes(10); + expect(mockSliderControl.setValue).toHaveBeenCalledTimes(10); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(10); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(20); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(100); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(200); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(45); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(0.8); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(1); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(2); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(3); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(4); }); it("should unlink DOM elements when no elements are selected", () => { eventBus.emit("selection:changed", { selectedElements: [] }); - expect(mockSliderControl.unlinkEvents).toHaveBeenCalledTimes(10); + expect(mockSliderControl.disable).toHaveBeenCalledTimes(10); }); it("should update slider controls on recalculate transform box", () => { @@ -117,12 +118,12 @@ describe("TransformMenu", () => { }; eventBus.emit("transformBox:properties:change", payload); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(10); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(20); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(100); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(200); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(45); - expect(mockSliderControl.updateValues).toHaveBeenCalledWith(0.8); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(10); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(20); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(100); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(200); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(45); + expect(mockSliderControl.setValue).toHaveBeenCalledWith(0.8); }); it("should emit transformBox:updatePosition on X position change", () => {