Compare commits

...
Author SHA1 Message Date
CrossPr0duct e99baccdb2 Brush works with colors need size slider. 2025-02-24 21:07:47 -05:00
CrossPr0duct f821d12cdf Fixed State Issues, should be good to go. 2025-02-24 21:02:19 -05:00
CrossPr0duct ffd52845e5 Fix rerender during undo and redo stack. 2025-02-24 20:31:31 -05:00
CrossPr0duct 4aece4045f Fixed bug with Transform manager. 2025-02-24 19:32:21 -05:00
CrossPr0duct b08731305d Painting Mode seems to work.
Need to hook into the transform tool and make sure rendering happens on changes.
2025-02-24 18:53:23 -05:00
CrossPr0duct ad078ed9f4 Refactor a bit now manage states. 2025-02-24 17:58:39 -05:00
CrossPr0duct 8250ea0b4a Ok fixing some code.
Need to ensure transform works.
2025-02-24 17:24:02 -05:00
CrossPr0duct fb87cdaee4 Painting Works. 2025-02-24 17:20:22 -05:00
CrossPr0duct 0f8e7dae82 Change Paint Color 2025-02-24 14:12:40 -05:00
CrossPr0duct 5668a43fc5 Adding Paint Mode.
Need to add an eraser.
2025-02-24 00:20:48 -05:00
ae6434e11e Add mode switching: SELECT, PAINT, ERASE (#420)
Co-authored-by: BFlat <bombayapirat@gmail.com>
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-02-23 21:30:00 -05:00
43 changed files with 1017 additions and 330 deletions
+94 -36
View File
@@ -1,8 +1,8 @@
import Konva from "konva";
import { uiAccess, uiEvents } from "~/signals";
import { ShapeNode } from "./Nodes";
import { PaintNode } from "./Nodes/PaintNode";
import { UndoStackManager } from "./UndoRedo";
import { CommandManager, MatteBox, SceneManager } from "./EngineUtitlities";
@@ -99,7 +99,6 @@ export class Engine {
this.stage.add(this.nodeIsolationLayer);
this.stage.add(this.uiLayer);
// Konva Transformer
this.nodeTransformer = new NodeTransformer();
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
@@ -112,17 +111,8 @@ export class Engine {
});
// core layer for all the work done.
this.offScreenCanvas = new OffscreenCanvas(0, 0);
this.realTimeDrawEngine = new RealTimeDrawEngine({
width: VideoResolutions.SQUARE_1024.width,
height: VideoResolutions.SQUARE_1024.height,
mediaLayerRef: this.mediaLayer,
bgLayerRef: this.bgLayer,
offScreenCanvas: this.offScreenCanvas
});
// Collection of all Nodes
this.nodesManager = new NodesManager();
// Partial Collection of selected Nodes
@@ -130,6 +120,18 @@ export class Engine {
nodeTransformerRef: this.nodeTransformer,
mediaLayerRef: this.mediaLayer,
});
this.realTimeDrawEngine = new RealTimeDrawEngine({
width: VideoResolutions.SQUARE_1024.width,
height: VideoResolutions.SQUARE_1024.height,
mediaLayerRef: this.mediaLayer,
offScreenCanvas: this.offScreenCanvas,
onDraw:async (canvas, lineBounds) => {
await this.addPaintNode(canvas, lineBounds);
},
});
// Selector Square to select Nodes
this.selectorSquare = new SelectorSquare({
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
@@ -179,19 +181,31 @@ export class Engine {
// some of the managers has events
// hence, lastly, setup these events
this.setupEventSystem();
this.setAppMode(AppModes.SELECT);
this.setAppMode(this.appMode);
}
private setAppMode(newAppMode: AppModes) {
this.appMode = newAppMode;
switch (this.appMode) {
case AppModes.PAINT: {
console.log("APPMODE: PAINT");
this.selectorSquare.disable(); // this breaks the paint if you use it in the wrong spot
this.selectionManager.disable(); // this prevents selection.
this.realTimeDrawEngine.enablePaintMode();
this.realTimeDrawEngine.disableDragging();
uiAccess.toolbarMain.enable();
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
active: false,
});
this.matteBox.disable();
return;
}
case AppModes.SELECT: {
console.log("APPMODE: SELECT");
this.realTimeDrawEngine.disablePaintMode();
this.realTimeDrawEngine.enableDragging();
this.selectorSquare.enable();
this.selectionManager.enable();
uiAccess.toolbarMain.enable();
@@ -365,10 +379,24 @@ export class Engine {
// Listen to Toolbar Main
uiEvents.toolbarMain.UNDO.onClick(() => this.undoStackManager.undo());
uiEvents.toolbarMain.REDO.onClick(() => this.undoStackManager.redo());
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
await this.realTimeDrawEngine.saveOutput();
});
uiEvents.toolbarMain.SELECT.onClick(() => {
console.log("Toolbar Main >> Select");
this.setAppMode(AppModes.SELECT)
});
uiEvents.toolbarMain.PAINT.onClick(() => {
console.log("Toolbar Main >> Paint");
this.setAppMode(AppModes.PAINT)
});
uiEvents.toolbarMain.ERASER.onClick(() => {
console.log("Toolbar Main >> Eraser");
});
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
@@ -399,8 +427,6 @@ export class Engine {
this.addImage(image);
});
uiEvents.onAddTextToEngine((textdata) => {
this.addText(textdata);
});
@@ -441,6 +467,7 @@ export class Engine {
});
uiEvents.onAddShapeToEngine((shapeData) => {
this.setAppMode(AppModes.SELECT);
switch (shapeData.shape) {
case "circle":
this.addShape(ShapeType.CIRCLE, 100);
@@ -465,7 +492,10 @@ export class Engine {
return;
}
this.changeNodeColor(nodeColor);
})
});
uiEvents.toolbarMain.onPaintColorChanged((color)=> {
this.realTimeDrawEngine.paintColor = color;
});
}
disableAllButtons() {
@@ -501,7 +531,7 @@ export class Engine {
}
// Sandbox is quickly a way to test your idea.
public async sandbox() { }
public async sandbox() {}
public onMessage(event: MessageEvent) {
console.log("Message From Shared Worker");
@@ -529,9 +559,9 @@ export class Engine {
x: 10,
y: 80,
text: "",
fontSize: 24,
fontSize: 18,
fontFamily: "Source Sans 3",
fill: "black",
fill: "white",
});
const anim = new Konva.Animation((frame) => {
if (frame && import.meta.env.DEV) {
@@ -549,28 +579,33 @@ export class Engine {
}
private changeNodeColor(nodeColor: NodeColor) {
console.log('Changing node color:', nodeColor);
console.log("Changing node color:", nodeColor);
// Find the node that is selected with the id
const idSelector = '#' + nodeColor.kNodeId;
const idSelector = "#" + nodeColor.kNodeId;
const selectedNode = this.mediaLayer.findOne(idSelector);
console.log('Selected node:', selectedNode);
console.log('ID selector:', idSelector);
console.log('Media layer:', this.mediaLayer);
console.log("Selected node:", selectedNode);
console.log("ID selector:", idSelector);
console.log("Media layer:", this.mediaLayer);
// TODO: Change the color of the node
// Turns out shapes are image nodes... how do we change the image's fill??
if (selectedNode) {
// Get the shape node from the real time drawing engine's image nodes
const shapeNode = this.realTimeDrawEngine.findImageNodeById(nodeColor.kNodeId);
const shapeNode = this.realTimeDrawEngine.findImageNodeById(
nodeColor.kNodeId,
);
if (shapeNode && shapeNode instanceof ShapeNode) {
// Create new shape node with updated color
const newShapeNode = new ShapeNode({
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
shapeType: shapeNode.shapeType,
size: shapeNode.size,
color: nodeColor.color,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager
selectionManagerRef: this.selectionManager,
loaded: async ()=> {
this.realTimeDrawEngine.render();
}
});
newShapeNode.kNode.position(shapeNode.kNode.position());
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
@@ -601,14 +636,19 @@ export class Engine {
size: size,
color: color,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
},
});
shapeNode.kNode.zIndex(1); // Replace desiredZIndex with a number
this.commandManager.createNode(shapeNode);
console.debug("Added shapenode:", shapeNode);
console.debug("Added node's ID:", shapeNode.kNode._id);
//this.realTimeDrawEngine.addNodes(shapeNode);
}
public addImage(imageFile: File) {
@@ -618,11 +658,29 @@ export class Engine {
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
imageFile: imageFile,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render();
},
});
imageNode.kNode.zIndex(1);
this.commandManager.createNode(imageNode);
//this.realTimeDrawEngine.addNodes(imageNode);
}
public addPaintNode(canvas: HTMLCanvasElement,lineBounds:{
width: number;
height: number;
x: number;
y: number;}) {
var node = new PaintNode({
canvasElement:canvas,
lineBounds:lineBounds,
mediaLayerRef: this.mediaLayer,
selectionManagerRef: this.selectionManager,
loaded: async () => {
await this.realTimeDrawEngine.render()
}})
this.commandManager.createNode(node);
}
public addVideo(
videNodeData: Partial<VideoNodeData> & { mediaFileUrl: string },
@@ -665,7 +723,7 @@ export class Engine {
if (
longestVideoNode === undefined ||
node.videoComponent.duration >
longestVideoNode.videoComponent.duration
longestVideoNode.videoComponent.duration
) {
longestVideoNode = node;
}
@@ -20,14 +20,15 @@ import {
NodeTransformer,
SelectionManager,
} from "../NodesManagers";
import { RenderEngine } from "../RenderingPrimitives/RenderEngine";
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
import { PaintNode } from "../Nodes/PaintNode";
interface EngineReferences {
mediaLayerRef: Konva.Layer;
nodesManagerRef: NodesManager;
nodeTransformerRef: NodeTransformer;
selectionManagerRef: SelectionManager;
renderEngineRef: RenderEngine;
renderEngineRef: RealTimeDrawEngine;
undoStackManagerRef: UndoStackManager;
}
@@ -36,7 +37,7 @@ export class CommandManager {
private nodesManagerRef: NodesManager;
private nodeTransformerRef: NodeTransformer;
private selectionManagerRef: SelectionManager;
private renderEngineRef: RenderEngine;
private renderEngineRef: RealTimeDrawEngine;
private undoStackManagerRef: UndoStackManager;
constructor(engineRefs: EngineReferences) {
@@ -48,7 +49,8 @@ export class CommandManager {
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
}
createNode(node: VideoNode | ImageNode | TextNode | ShapeNode) {
createNode(node: MediaNode) {
const command = new CreateCommand({
nodes: new Set<MediaNode>([node]),
mediaLayerRef: this.mediaLayerRef,
@@ -62,6 +64,7 @@ export class CommandManager {
// Set the kNode's manual id to same as kNode's internal _id
// This helps identify the kNode in the Konva layer with find
node.kNode.id(node.kNode._id.toString());
this.renderEngineRef.render();
}
deleteNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
@@ -74,6 +77,7 @@ export class CommandManager {
renderEngineRef: this.renderEngineRef,
});
this.undoStackManagerRef.executeCommand(command);
this.renderEngineRef.render();
}
toggleLockNodes() {
const nodes = this.selectionManagerRef.getSelectedNodes();
@@ -101,6 +105,7 @@ export class CommandManager {
mediaLayerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.executeCommand(command);
this.renderEngineRef.render();
}
moveNodesDown() {
const command = new MoveLayerDown({
@@ -109,6 +114,7 @@ export class CommandManager {
mediaLayerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.executeCommand(command);
this.renderEngineRef.render();
}
translateNodes(props: {
nodes: Set<MediaNode>;
@@ -120,6 +126,7 @@ export class CommandManager {
layerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.pushCommand(command);
}
transformNodes(props: {
nodes: Set<MediaNode>;
@@ -131,6 +138,7 @@ export class CommandManager {
layerRef: this.mediaLayerRef,
});
this.undoStackManagerRef.pushCommand(command);
this.renderEngineRef.render();
}
addChromaKey(props: { videoNode: VideoNode; newChromaColor: RGBColor }) {
const command = new AddChromaKey(props);
@@ -68,7 +68,7 @@ export abstract class BaseNode {
if (this.kNode instanceof Konva.Group) {
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
console.log("Wrapper rect:", wrapperRect);
if (wrapperRect !=null) {
if (wrapperRect != null) {
wrapperRect.stroke(primaryOrange);
wrapperRect.strokeWidth(highlightStrokeWidth);
}
@@ -89,8 +89,8 @@ export abstract class BaseNode {
}
if (this.kNode instanceof Konva.Group) {
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
console.log("Wrapper rect:", wrapperRect);
if (wrapperRect !=null) {
console.log("Wrapper rect:", wrapperRect);
if (wrapperRect != null) {
// Issue investigate
wrapperRect.strokeWidth(0);
}
@@ -138,7 +138,7 @@ export abstract class BaseNode {
// console.log("handle select");
if (!isMultiSelect) {
// clear selection if not multislect
//console.log("No Shift >> no multiselect");
console.log("No Shift >> no multiselect");
this.selectionManagerRef.clearSelection();
}
if (this.selectionManagerRef.isNodeSelected(this)) {
@@ -151,8 +151,25 @@ export abstract class BaseNode {
}
};
const handleRightClick = () => {
this.selectionManagerRef.clearSelection();
this.selectionManagerRef.selectNode(this, undefined, true);
}
this.kNode.on("contextmenu", (e) => {
console.log("CONTEXT MENU", e);
e.evt.preventDefault();
handleRightClick();
});
this.kNode.on("mousedown", (e) => {
// console.log("MOUSE DOWN");
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
// 3: Right mouse button
// If right click, let context event handle it
if (e.evt.button === 3) {
return;
}
// Selection of Node
if (!this.selectionManagerRef.isNodeSelected(this)) {
this.isSelecting = true;
@@ -162,7 +179,13 @@ export abstract class BaseNode {
});
this.kNode.on("mouseup", (e) => {
// console.log("MOUSE UP");
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
// 3: Right mouse button
// If right click, let context event handle it
if (e.evt.button === 3) {
return;
}
// just coming out of dragging or selecting mode
// no need to handle select
if (this.selectionManagerRef.isDragging() || this.isSelecting) {
@@ -17,12 +17,13 @@ interface ImageNodeContructor {
transform?: TransformationData;
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class ImageNode extends NetworkedNode {
public kNode: Konva.Image;
public imageSize?: Size;
private loaded!: () => Promise<void>;
constructor({
canvasPosition,
canvasSize,
@@ -32,6 +33,7 @@ export class ImageNode extends NetworkedNode {
transform: existingTransform,
mediaLayerRef,
selectionManagerRef,
loaded,
}: ImageNodeContructor) {
// kNodes need to be created first to guaruntee
// that it is not undefined in parent's context
@@ -53,6 +55,7 @@ export class ImageNode extends NetworkedNode {
localFile: imageFile,
});
this.kNode = kNode;
this.loaded = loaded;
// this.imageSize = minNodeSize;
this.mediaLayerRef.add(this.kNode);
@@ -108,6 +111,7 @@ export class ImageNode extends NetworkedNode {
this.listenToBaseKNode();
this.mediaLayerRef.draw();
//this.uploadImage(imageFile);
this.loaded();
};
imageComponent.onerror = () => {
//this.setProgress({ progress: 0, status: UploadStatus.ERROR_ON_FILE });
@@ -0,0 +1,92 @@
import Konva from "konva";
import { SelectionManager } from "../NodesManagers";
import { Position, NodeData } from "../types";
import { NodeType } from "./constants";
import { NetworkedNode } from "./NetworkedNode";
interface PaintNodeConstructor {
canvasElement:HTMLCanvasElement;
lineBounds: {
width: number;
height: number;
x: number;
y: number;
};
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class PaintNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
constructor({
canvasElement,
lineBounds,
mediaLayerRef,
selectionManagerRef,
loaded,
}: PaintNodeConstructor) {
const cl = canvasElement
// Convert circle to image
const dataURL = cl.toDataURL();
const imageObj = new Image();
imageObj.src = dataURL;
const paintShape = new Konva.Image({
x: lineBounds.x,
y: lineBounds.y,
width: lineBounds.width,
height: lineBounds.height,
image: imageObj,
listening: false, // Disable event listening to allow captureCanvas to receive events
draggable: true,
// stroke: '#000000',
// strokeWidth: 2,
// dash: [5, 5], // Creates dotted outline THIS IS FOR DEBUG
globalCompositeOperation: 'source-over',
fill: 'transparent'
});
imageObj.onload = () => {
this.mediaLayerRef.draw();
loaded();
};
super({
selectionManagerRef: selectionManagerRef,
mediaLayerRef: mediaLayerRef,
kNode: paintShape,
});
this.mediaLayerRef.add(this.kNode);
this.listenToBaseKNode();
this.mediaLayerRef.draw();
}
public getNodeData(canvasPosition: Position) {
const data: NodeData = {
type: NodeType.SHAPE,
transform: {
position: {
x: this.kNode.position().x - canvasPosition.x,
y: this.kNode.position().y - canvasPosition.y,
},
size: this.kNode.size(),
rotation: this.kNode.rotation(),
scale: {
x: this.kNode.scaleX(),
y: this.kNode.scaleY(),
},
zIndex: this.kNode.getZIndex(),
},
};
return data;
}
}
@@ -20,10 +20,10 @@ interface ShapeNodeConstructor {
transform?: TransformationData;
mediaLayerRef: Konva.Layer;
selectionManagerRef: SelectionManager;
loaded: () => Promise<void>;
}
export class ShapeNode extends NetworkedNode {
retry(): void {
throw new Error("Method not implemented.");
}
@@ -36,10 +36,11 @@ export class ShapeNode extends NetworkedNode {
canvasSize,
shapeType,
size,
color = "#ff0000", // Default red if no color provided
color = "#2D81FF", // Default red if no color provided
transform: existingTransform,
mediaLayerRef,
selectionManagerRef,
loaded,
}: ShapeNodeConstructor) {
// Create the actual shape inside the group
const transform = NodeUtilities.getInitialTransform({
@@ -49,7 +50,7 @@ export class ShapeNode extends NetworkedNode {
});
let shape = null;
switch (shapeType) {
case ShapeType.CIRCLE:
const circleShape = new Konva.Circle({
@@ -72,13 +73,14 @@ export class ShapeNode extends NetworkedNode {
width: circleShape.width(),
height: circleShape.height(),
...transform,
fill: 'transparent',
fill: "transparent",
strokeScaleEnabled: false,
draggable: true,
});
imageObj.onload = () => {
this.mediaLayerRef.draw();
loaded();
};
// Clean up the temporary circle
@@ -107,13 +109,14 @@ export class ShapeNode extends NetworkedNode {
width: squareShape.width(),
height: squareShape.height(),
...transform,
fill: 'transparent',
fill: "transparent",
strokeScaleEnabled: false,
draggable: true,
});
squareImageObj.onload = () => {
this.mediaLayerRef.draw();
loaded()
};
// Clean up the temporary square
@@ -142,13 +145,14 @@ export class ShapeNode extends NetworkedNode {
width: triangleShape.width(),
height: triangleShape.height(),
...transform,
fill: 'transparent',
fill: "transparent",
strokeScaleEnabled: false,
draggable: true,
});
triangleImageObj.onload = () => {
this.mediaLayerRef.draw();
loaded()
};
// Clean up the temporary triangle
@@ -4,12 +4,12 @@ export enum NodeType {
IMAGE = "image",
TEXT = "text",
VIDEO = "video",
SHAPE = "shape"
SHAPE = "shape",
}
export const transparent = Colors.transparent;
export const primaryOrange = Colors.primaryOrange;
export const highlightStrokeWidth = 10;
export const highlightStrokeWidth = 2;
export const minNodeSize = {
width: 200,
height: 200,
@@ -80,7 +80,7 @@ export class SelectionManager {
public isDisabled() {
return this.disable;
}
public selectNode(node: MediaNode, doNotDraw?: boolean): boolean {
public selectNode(node: MediaNode, doNotDraw?: boolean, showContextMenu = false): boolean {
if (
this.disabled || // no-op when disabled
this.selectedNodes.has(node) || // if the node is already selected
@@ -100,7 +100,11 @@ export class SelectionManager {
this.selectedNodes.add(node);
this.updateNodeTransformer();
this.updateContextComponents();
this.showContextComponents();
console.log("SelectionManager > selectNode > showContextMenu", showContextMenu);
if (showContextMenu) {
this.showContextComponents();
}
if (!doNotDraw) {
this.mediaLayerRef.batchDraw();
@@ -189,8 +193,8 @@ export class SelectionManager {
// done, clear data and show menu
this.initialPositions.clear();
this.updateContextComponents();
this.showContextComponents();
}
private getKNodeTransformation(kNode: Konva.Node) {
return {
kNodeId: kNode._id,
@@ -40,8 +40,9 @@ export class SelectorSquare {
stageRef: Konva.Stage;
}) {
this.kSquare = new Konva.Rect({
fill: " rgb(250, 128, 114, 0.3)",
stroke: "salmon",
fill: "rgba(45, 129, 255, 0.3)",
stroke: "#00a1ff",
strokeWidth: 1,
visible: false,
// disable events to not interrupt with events
listening: false,
@@ -11,12 +11,15 @@ import { MediaNode } from "../types";
import { RenderTask } from "./RenderTask";
import { OffScreenSceneCanvas } from "./OffScreenSceneCanvas";
import { Image } from "@tauri-apps/api/image";
import { PaintNode } from "../Nodes/PaintNode";
// https://www.aiseesoft.com/resource/phone-aspect-ratio-screen-resolution.html#:~:text=16%3A9%20Aspect%20Ratio
export class RealTimeDrawEngine {
private videoNodes: VideoNode[];
private imageNodes: (ImageNode | TextNode | ShapeNode)[];
private imageNodes: (ImageNode | TextNode | ShapeNode | PaintNode)[];
private offScreenCanvas: OffscreenCanvas;
private outputBitmap: ImageBitmap | undefined;
@@ -26,6 +29,7 @@ export class RealTimeDrawEngine {
// capturing composite within window
private mediaLayerRef: Konva.Layer;
private drawingsLayer: Konva.Layer; // New Layer for Drawings
private height: number;
private width: number;
@@ -46,20 +50,40 @@ export class RealTimeDrawEngine {
public currentPrompt: string;
public currentStrength: number;
// Paint Color
// paint Brush Size
// has to exit out of paint mode when shape or image are used.
public paintColor: string = "#000000";
public paintBrushSize: number = 5;
public isPaintMode: boolean = false;
private onDrawCallback?: (canvas: HTMLCanvasElement,lineBounds:{
width: number;
height: number;
x: number;
y: number;}) => void;
constructor({
width,
height,
mediaLayerRef,
offScreenCanvas,
onDraw,
}: {
width: number;
height: number;
mediaLayerRef: Konva.Layer;
offScreenCanvas: OffscreenCanvas;
onDraw?: (canvas: HTMLCanvasElement,lineBounds:{
width: number;
height: number;
x: number;
y: number;}) => void;
}) {
this.videoLoadingCanvas = undefined;
this.videoNodes = [];
this.imageNodes = [];
this.onDrawCallback = onDraw
// TODO: Make this dynamic and update this on change of canvas.
@@ -69,7 +93,8 @@ export class RealTimeDrawEngine {
this.positionX = window.innerWidth / 2 - this.width / 2 - this.width;
this.positionY = window.innerHeight / 2 - this.height / 2;
this.positionPreviewX = window.innerWidth / 2 - this.width / 2 + this.width;
this.positionPreviewX =
window.innerWidth / 2 - this.width / 2 + this.width;
this.positionPreviewY = window.innerHeight / 2 - this.height / 2;
this.offScreenCanvas = offScreenCanvas;
@@ -79,6 +104,12 @@ export class RealTimeDrawEngine {
// this is the whole canvas
this.mediaLayerRef = mediaLayerRef;
// Create a separate layer for drawings
this.drawingsLayer = new Konva.Layer({
clearBeforeDraw: true, // Ensures transparent background
});
this.mediaLayerRef.getStage()?.add(this.drawingsLayer);// to od pass in stage
// Set background layer to red and media layer to green for visibility
this.port = undefined;
@@ -123,15 +154,178 @@ export class RealTimeDrawEngine {
}
private isEnabled: boolean = false;
private cleanupFunction: (() => void) | null = null;
public paintMode() {
let isDrawing = false;
let currentLine: Konva.Line | null = null;
const startDrawing = (pos: { x: number; y: number }) => {
if (!this.isEnabled) return;
const stage = this.mediaLayerRef.getStage();
if (!stage) return;
// Convert pointer position to relative position within capture canvas
const captureBox = this.captureCanvas.getClientRect();
const relativeX = pos.x - captureBox.x;
const relativeY = pos.y - captureBox.y;
currentLine = new Konva.Line({
points: [relativeX, relativeY],
stroke: this.paintColor,
strokeWidth: 5,
lineCap: "round",
lineJoin: "round",
x: this.captureCanvas.x(),
y: this.captureCanvas.y(),
draggable: false,
});
this.drawingsLayer.add(currentLine); // Add to drawingsLayer
isDrawing = true;
};
const draw = (pos: { x: number; y: number }) => {
if (!this.isEnabled) return;
if (!isDrawing || !currentLine) return;
// Convert pointer position to relative position
const captureBox = this.captureCanvas.getClientRect();
const relativeX = pos.x - captureBox.x;
const relativeY = pos.y - captureBox.y;
const newPoints = currentLine.points().concat([relativeX, relativeY]);
currentLine.points(newPoints);
this.drawingsLayer.batchDraw();
};
const stopDrawing = () => {
if (!this.isEnabled) return;
if (!isDrawing || !currentLine) return;
// Store current line before resetting state
const lineToConvert = currentLine;
// Reset drawing state immediately so we can start a new stroke
isDrawing = false;
currentLine = null;
// Create a temporary layer to render the line
const tempLayer = new Konva.Layer({
clearBeforeDraw: true,
});
this.mediaLayerRef.getStage()?.add(tempLayer);
tempLayer.add(lineToConvert);
// Get the line canvas with transparent background
// Get the bounding box of the line
const lineBounds = lineToConvert.getClientRect();
// Create canvas with just enough size to contain the line
const lineCanvas = tempLayer.toCanvas({
x: lineBounds.x,
y: lineBounds.y,
width: lineBounds.width,
height: lineBounds.height,
pixelRatio: 1,
});
if (this.onDrawCallback) {
this.onDrawCallback(lineCanvas, lineBounds);
}
// Clean up
lineToConvert.destroy();
tempLayer.destroy();
this.drawingsLayer.batchDraw();
};
// Check if point is within capture canvas bounds
const isWithinCaptureCanvas = (pos: { x: number; y: number }) => {
const captureBox = this.captureCanvas.getClientRect();
return (
pos.x >= captureBox.x &&
pos.x <= captureBox.x + captureBox.width &&
pos.y >= captureBox.y &&
pos.y <= captureBox.y + captureBox.height
);
};
// Add event listeners
const stage = this.mediaLayerRef.getStage();
if (!stage) return;
stage.on("mousedown touchstart", (e) => {
const pos = stage.getPointerPosition();
if (pos && isWithinCaptureCanvas(pos)) {
startDrawing(pos);
}
});
stage.on("mousemove touchmove", (e) => {
const pos = stage.getPointerPosition();
if (pos && isWithinCaptureCanvas(pos)) {
draw(pos);
}
});
stage.on("mouseup touchend", async () => {
stopDrawing();
await this.render();
});
// Store cleanup function
this.cleanupFunction = () => {
stage.off("mousedown touchstart");
stage.off("mousemove touchmove");
stage.off("mouseup touchend");
};
}
public enablePaintMode() {
this.isEnabled = true;
if (!this.cleanupFunction) {
this.paintMode();
}
}
public enableDragging() {
// Enable dragging for all nodes in media layer
this.imageNodes?.forEach((node) => {
node.kNode.draggable(true);
node.kNode.listening(true);
});
this.mediaLayerRef.batchDraw();
}
public disableDragging() {
// Disable dragging for all nodes in media layer
this.imageNodes?.forEach((node) => {
node.kNode.draggable(false);
node.kNode.listening(false);
});
this.mediaLayerRef.batchDraw();
}
public disablePaintMode() {
this.isEnabled = false;
if (this.cleanupFunction) {
this.cleanupFunction();
this.cleanupFunction = null;
}
}
public previewCopyListener() {
this.previewCanvas.on('mousedown touchstart', () => {
this.previewCanvas.on("mousedown touchstart", () => {
if (!this.outputBitmap) {
console.log("No preview image to copy");
console.log("No preview image to copy");
return;
}
// TODO create it as a node.
// const imageNode = new ImageNode({
// mediaLayerRef: this.mediaLayer,
// canvasPosition: this.renderEngine.captureCanvas.position(),
@@ -148,42 +342,41 @@ export class RealTimeDrawEngine {
height: this.height,
image: this.outputBitmap,
draggable: true,
listening: true
listening: true,
});
this.mediaLayerRef.add(previewCopy);
previewCopy.moveToTop();
this.mediaLayerRef.draw();
this.drawingsLayer.add(previewCopy); // Add to drawingsLayer instead of mediaLayerRef
this.drawingsLayer.batchDraw();
// Start dragging immediately
previewCopy.startDrag();
// Handle drag events
previewCopy.on('dragmove', () => {
this.mediaLayerRef.draw();
previewCopy.on("dragmove", () => {
this.drawingsLayer.draw();
});
previewCopy.on('dragend', () => {
previewCopy.on("dragend", () => {
const previewBox = previewCopy.getClientRect();
const captureBox = this.captureCanvas.getClientRect();
if (Konva.Util.haveIntersection(previewBox, captureBox)) {
// Snap to capture canvas position
previewCopy.position({
x: this.captureCanvas.x(),
y: this.captureCanvas.y()
y: this.captureCanvas.y(),
});
this.mediaLayerRef.batchDraw();
this.drawingsLayer.batchDraw();
} else {
// Remove if dropped outside capture area
previewCopy.destroy();
this.mediaLayerRef.batchDraw();
this.drawingsLayer.batchDraw();
}
});
});
}
public findImageNodeById(id: string): (ImageNode | TextNode | ShapeNode | undefined) {
public findImageNodeById(id: string): (ImageNode | TextNode | ShapeNode | PaintNode | undefined) {
return this.imageNodes.find(node => {
if (node.kNode) {
return node.kNode.id() === id;
@@ -327,6 +520,7 @@ export class RealTimeDrawEngine {
private handleNodeDragEnd = async () => {
// Clean up any existing state
if (this.isProcessing) {
console.log("isProcessing Returning")
return;
}
console.log("Node drag ended");
@@ -334,14 +528,24 @@ export class RealTimeDrawEngine {
await this.render();
};
public addNodes(node: MediaNode) {
if (node instanceof ImageNode || node instanceof TextNode || node instanceof ShapeNode) {
public async addNodes(node: MediaNode) {
if (node instanceof ImageNode ||
node instanceof TextNode ||
node instanceof ShapeNode ||
node instanceof PaintNode)
{
console.debug("Adding node:", node);
this.imageNodes.push(node);
console.log(this.imageNodes)
node.kNode.on("dragend", this.handleNodeDragEnd);
}
// ensure the layer doesn't move if added while painting.
if (this.isEnabled) {
this.disableDragging();
}
await this.render();
}
public removeNodes(node: MediaNode) {
@@ -351,7 +555,7 @@ export class RealTimeDrawEngine {
node.kNode.off("dragend", this.handleNodeDragEnd);
this.videoNodes.splice(index, 1);
}
} else if (node instanceof ImageNode || node instanceof TextNode) {
} else if (node instanceof ImageNode || node instanceof TextNode || node instanceof ShapeNode || node instanceof PaintNode) {
const index = this.imageNodes.indexOf(node);
if (index > -1) {
node.kNode.off("dragend", this.handleNodeDragEnd);
@@ -512,6 +716,7 @@ export class RealTimeDrawEngine {
public async render() {
// only pick nodes that intersect wi th the canvas on screen bounds to freeze.
this.mediaLayerRef.draw();
// Output all nodes in mediaLayerRef
const nodes = this.mediaLayerRef.getChildren();
@@ -533,12 +738,12 @@ export class RealTimeDrawEngine {
})) as ImageBitmap;
// Test code
if (true) {
this.outputBitmap = bitmap;
this.previewCanvas.image(bitmap);
this.isProcessing = false;
return;
}
// if (true) {
// this.outputBitmap = bitmap;
// this.previewCanvas.image(bitmap);
// this.isProcessing = false;
// return;
// }
try {
const base64Bitmap = await this.imageBitmapToBase64(bitmap);
@@ -6,7 +6,7 @@ import {
NodeTransformer,
SelectionManager,
} from "../NodesManagers";
import { RenderEngine } from "../RenderingPrimitives/RenderEngine";
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
import { toolbarNode } from "~/signals/uiAccess/toolbarNode";
@@ -19,7 +19,7 @@ export class CreateCommand implements ICommand {
private nodeTransformerRef: NodeTransformer;
private selectionManagerRef: SelectionManager;
private renderEngineRef: RenderEngine;
private renderEngineRef: RealTimeDrawEngine;
constructor({
nodes,
@@ -33,7 +33,7 @@ export class CreateCommand implements ICommand {
mediaLayerRef: Konva.Layer;
nodesManagerRef: NodesManager;
nodeTransformerRef: NodeTransformer;
renderEngineRef: RenderEngine;
renderEngineRef: RealTimeDrawEngine;
selectionManagerRef: SelectionManager;
}) {
this.nodes = nodes;
@@ -53,6 +53,7 @@ export class CreateCommand implements ICommand {
this.renderEngineRef.addNodes(node);
});
this.mediaLayerRef.draw();
}
undo() {
@@ -71,5 +72,7 @@ export class CreateCommand implements ICommand {
this.nodeTransformerRef.clear();
toolbarNode.hide();
this.mediaLayerRef.draw();
this.renderEngineRef.render();
}
}
+3 -1
View File
@@ -2,6 +2,8 @@ export enum AppModes {
INIT = "INIT",
PREVIEW = "PREVIEW",
SELECT = "SELECT",
PAINT = "PAINT",
ERASER = "ERASER",
RENDERING = "RENDERING",
}
@@ -14,5 +16,5 @@ export const VideoResolutions = {
export const Colors = {
transparent: "rgba(0,0,0,0)",
translucentBlack: "rgba(0,0,0,0.3)",
primaryOrange: "rgba(230,100,98,1)",
primaryOrange: "rgba(45, 129, 255, 1)",
};
@@ -3,10 +3,11 @@ import { NodeType, BaseNode, ImageNode, TextNode, VideoNode, ShapeNode } from ".
import { TransformationData } from "./Transformation";
import { TextNodeData } from "./Text";
import { PaintNode } from "../Nodes/PaintNode";
export type MediaNode =
// | NetworkedNode
BaseNode | VideoNode | ImageNode | TextNode | ShapeNode;
BaseNode | VideoNode | ImageNode | TextNode | ShapeNode | PaintNode;
export type ImageNodeData = {
mediaFileUrl: string;
@@ -1,22 +1,28 @@
import { useSignalEffect } from "@preact/signals-react";
import { Slider } from "~/components/ui"
import { dispatchUiEvents } from "~/signals"
import { Slider } from "~/components/ui";
import { dispatchUiEvents } from "~/signals";
import { promptStrength } from "~/signals/uiEvents/promptSettings";
export const SignaledPromptSlider = () => {
const sliderValue = promptStrength.value;
const onSliderValueChanged =
dispatchUiEvents.promptSettings.setPromptStrength;
const sliderValue = promptStrength.value
const onSliderValueChanged = dispatchUiEvents.promptSettings.setPromptStrength
// This makes component re-render whenever the signal value changes
useSignalEffect(() => {
promptStrength.value;
});
// This makes component re-render whenever the signal value changes
useSignalEffect(() => {
promptStrength.value;
})
return (
<div className="fixed bottom-80 left-1/2 -translate-x-1/2 h-4 w-96">
<Slider min={0} max={100} value={sliderValue} onChange={onSliderValueChanged} step={1} />
</div>
)
}
return (
<div className="fixed bottom-[138px] left-1/2 h-4 w-[400px] -translate-x-1/2 shadow-lg">
<Slider
min={0}
max={100}
value={sliderValue}
onChange={onSliderValueChanged}
step={1}
innerLabel={`Strength - ${sliderValue}%`}
/>
</div>
);
};
@@ -1,31 +1,17 @@
import { useSignalEffect } from "@preact/signals-react";
import { useCallback, useEffect, useRef } from "react";
import { useRef } from "react";
import { ResizeType, Textarea } from "~/components/ui";
import { dispatchUiEvents } from "~/signals";
import { promptText } from "~/signals/uiEvents/promptSettings";
export const SignaledPromptText = ({
}) => {
export const SignaledPromptText = ({}) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const focusOnTextArea = useCallback(() => {
if (textAreaRef.current === null) {
return;
}
textAreaRef.current.focus();
}, []);
useEffect(() => {
focusOnTextArea();
}, []);
const TextAreaStates = {
style: {
width: "530px",
height: "200px",
},
placeholder: "",
placeholder: "Type your prompt here...",
label: "",
resize: "none" as ResizeType,
rows: 3,
};
const textContent = promptText.value;
@@ -36,11 +22,18 @@ export const SignaledPromptText = ({
// This makes component re-render whenever the signal value changes
useSignalEffect(() => {
promptText.value;
})
});
return (
<div className="fixed bottom-24 left-1/2 -translate-x-1/2">
<Textarea {...TextAreaStates} ref={textAreaRef} onChange={onTextChanged} value={textContent} />
<div className="fixed bottom-5 left-1/2 w-[400px] -translate-x-1/2 text-[16px] font-medium shadow-lg">
<Textarea
className="glass rounded-xl"
{...TextAreaStates}
ref={textAreaRef}
onChange={onTextChanged}
value={textContent}
spellCheck={false}
/>
</div>
);
};
@@ -50,6 +50,13 @@ export const SignaledToolbarMain = ({
return () => dispatchUiEvents.addShapeToEngine({ shape: "triangle" });
case ToolbarMainButtonNames.ADD_SQUARE:
return () => dispatchUiEvents.addShapeToEngine({ shape: "square" });
case ToolbarMainButtonNames.SELECT:
case ToolbarMainButtonNames.ERASER:
case ToolbarMainButtonNames.PAINT:
return (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
toolbarMain.setMode(buttonName);
dispatchers[buttonName](e);
};
default:
return dispatchers[buttonName];
}
@@ -76,7 +83,7 @@ export const SignaledToolbarMain = ({
dispatchers.loadingBarRetry(e);
};
return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2">
<div className="fixed left-4 top-1/2 -translate-y-1/2">
<Transition
as="div"
className={twMerge(
+26 -3
View File
@@ -4,10 +4,11 @@
@layer base {
:root {
@apply bg-ui-background font-sans text-[#222222];
@apply bg-ui-background font-sans text-white;
}
.pegboard {
background-image: radial-gradient(#3f3f466b 1px, transparent 0);
background-image: radial-gradient(#ffffff25 1px, transparent 0);
background-color: #1b1b1f;
background-size: 36px 36px;
@apply bg-center;
}
@@ -41,7 +42,24 @@
}
input {
@apply border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
@apply bg-ui-controls border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
}
textarea {
@apply bg-ui-controls placeholder:text-white/50;
/* Webkit scrollbar styles */
&::-webkit-scrollbar {
@apply w-1;
}
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-full bg-white/20 hover:bg-white/30;
}
}
}
@@ -87,6 +105,11 @@
background-position: 100% 0;
animation: highlight-sweep 3s infinite linear;
}
.glass {
@apply relative border border-ui-border shadow-lg backdrop-blur-xl transition-colors duration-300;
@apply bg-ui-controls/50 hover:bg-ui-controls/90 !important;
}
}
@keyframes glow {
@@ -8,6 +8,7 @@ import { ToolbarUserProfile } from "~/components/features";
//Components of the Konva App are all in the KonvaComponent
import { KonvaRootComponent } from "~/KonvaRootComponent";
import { ToolbarTopLeft } from "~/components/features/ToolbarTopLeft";
import { ToolbarTopRight } from "~/components/features/ToolbarTopRight";
export const Main = withProtectionRoute(() => {
// This is a hook that will log the number of times the component has rerendered
@@ -25,6 +26,7 @@ export const Main = withProtectionRoute(() => {
/>
<div className="absolute top-0 flex w-full justify-between px-2">
<ToolbarTopLeft />
<ToolbarTopRight />
{/* <ToolbarUserProfile /> */}
</div>
</div>
@@ -52,7 +52,7 @@ export const DialogAddImage = ({
"w-full max-w-2xl",
)}
>
<DialogTitle className="text-3xl font-bold">Upload Image</DialogTitle>
<DialogTitle className="text-2xl font-bold">Upload Image</DialogTitle>
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
<FileUploader
title=""
@@ -73,7 +73,7 @@ export const DialogEditText = ({
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
<div className={dialogBackgroundStyles}>
<DialogPanel className={twMerge(paperWrapperStyles, dialogPanelStyles)}>
<DialogTitle className="text-3xl font-bold">Edit Text</DialogTitle>
<DialogTitle className="text-2xl font-bold">Edit Text</DialogTitle>
<TextEditor
text={text}
formatData={textFormatData}
@@ -24,7 +24,7 @@ export const DragAndDropZone = ({ file, fileTypes }: Props) => {
// const fileName = file && getFileName(file).toUpperCase();
const wrapperClassName = twMerge(
"group cursor-pointer px-6 py-12 bg-gray-100 hover:bg-gray-200/60 transition-colors duration-150 ease-in-out",
"group cursor-pointer px-6 py-12 bg-ui-controls hover:bg-white/25 transition-colors duration-150 ease-in-out rounded-lg",
!file && "flex flex-col items-center justify-center gap-",
file && "flex items-center gap-2 py-6",
// "rounded-lg border-2 border-dashed border-ui-border",
@@ -44,12 +44,12 @@ export const ToolbarButton = ({
}
const mergedButtonClasses = twMerge(
"rounded-lg py-2 px-3 hover:bg-gray-200/50 transition-all duration-100",
children ? "w-fit flex items-center gap-2 text-nowrap" : "size-10",
active && "pointer-events-none text-primary ",
"rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2",
children ? "w-fit flex items-center gap-2.5 text-nowrap" : "size-10",
active && "bg-primary/30 border-2 border-primary hover:bg-primary/30",
disabled && "pointer-events-none text-secondary-300",
buttonProps.prominent &&
"highlight-button border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
"border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
customButtonClassNames,
);
@@ -57,6 +57,7 @@ export const ToolbarButton = ({
<button
className={mergedButtonClasses}
disabled={disabled}
autoFocus={false}
{...restButtonProps}
onClick={(e) => {
e.preventDefault();
@@ -73,7 +74,11 @@ export const ToolbarButton = ({
</button>
);
if (tooltip) {
return <Tooltip tip={tooltip}>{Button}</Tooltip>;
return (
<Tooltip position="right" tip={tooltip}>
{Button}
</Tooltip>
);
}
return Button;
};
@@ -1,29 +1,30 @@
import { Fragment, MouseEventHandler } from "react";
import { twMerge } from "tailwind-merge";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowRotateLeft,
faArrowRotateRight,
faCircle,
faEraser,
faFilePlus,
faFilm,
faFloppyDisk,
faImage,
faLocationArrow,
faMagicWandSparkles,
faPaintbrush,
faShapes,
faSquare,
faText,
faTriangle,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment, MouseEventHandler } from "react";
import { twMerge } from "tailwind-merge";
import { ToolbarButton } from "../ToolbarButton";
// style and constants
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
import { ToolbarMainButtonNames } from "./enum";
import { ButtonPreviewAndRender } from "./ButtonPreviewAndRender";
import { dispatchUiEvents, uiEvents } from "~/signals/uiEvents";
import { paintColor } from "~/signals/uiEvents/toolbarMain/paintMode";
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
export const ToolbarMain = ({
disabled = false,
@@ -42,13 +43,14 @@ export const ToolbarMain = ({
<>
<div
className={twMerge(
"flex w-fit items-center divide-x divide-ui-border",
"flex h-fit flex-col items-center gap-y-2",
paperWrapperStyles,
"glass",
disabled &&
"pointer-events-none cursor-default bg-ui-border shadow-md",
"pointer-events-none cursor-default bg-ui-border shadow-md",
)}
>
<div className="flex items-center gap-2 pr-2">
<div className="flex flex-col items-center gap-2">
<ToolbarButton
icon={faLocationArrow}
iconProps={{ className: "fa-flip-horizontal" }}
@@ -62,62 +64,135 @@ export const ToolbarMain = ({
/>
<Popover className="relative">
<PopoverButton as={Fragment}>
<button
data-tooltip="Add Shapes"
className={twMerge(
"size-10 rounded-lg p-2 hover:bg-gray-200/50",
toolTipStyles,
)}
>
<FontAwesomeIcon icon={faShapes} />
</button>
</PopoverButton>
<PopoverPanel
anchor="bottom"
className={twMerge(
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
"flex flex-col [--anchor-gap:16px]",
paperWrapperStyles,
)}
>
<ToolbarButton icon={faSquare} buttonProps={buttonProps.ADD_SQUARE} />
<ToolbarButton icon={faCircle} buttonProps={buttonProps.ADD_CIRCLE} />
<ToolbarButton icon={faTriangle} buttonProps={buttonProps.ADD_TRIANGLE} />
</PopoverPanel>
{({ close }) => (
<>
<PopoverButton as={Fragment}>
<button
data-tooltip="Add Shapes"
className={twMerge(
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
toolTipStyles.base,
toolTipStyles.right,
)}
>
<FontAwesomeIcon icon={faShapes} />
</button>
</PopoverButton>
<PopoverPanel
anchor="right"
className={twMerge(
"flex flex-row [--anchor-gap:12px]",
paperWrapperStyles,
)}
>
<ToolbarButton
icon={faSquare}
buttonProps={{
...buttonProps.ADD_SQUARE,
onClick: (e) => {
buttonProps.ADD_SQUARE.onClick?.(e);
close();
},
}}
/>
<ToolbarButton
icon={faCircle}
buttonProps={{
...buttonProps.ADD_CIRCLE,
onClick: (e) => {
buttonProps.ADD_CIRCLE.onClick?.(e);
close();
},
}}
/>
<ToolbarButton
icon={faTriangle}
buttonProps={{
...buttonProps.ADD_TRIANGLE,
onClick: (e) => {
buttonProps.ADD_TRIANGLE.onClick?.(e);
close();
},
}}
/>
</PopoverPanel>
</>
)}
</Popover>
<Popover className="relative">
<PopoverButton as={Fragment}>
<button
data-tooltip="Add..."
data-tooltip="Add Media"
className={twMerge(
"size-10 rounded-lg p-2 hover:bg-gray-200/50",
toolTipStyles,
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
toolTipStyles.base,
toolTipStyles.right,
)}
>
<FontAwesomeIcon icon={faFilePlus} />
</button>
</PopoverButton>
<PopoverPanel
anchor="bottom"
anchor="right"
className={twMerge(
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
"flex flex-col [--anchor-gap:16px]",
"flex [--anchor-gap:12px]",
paperWrapperStyles,
)}
>
<ToolbarButton icon={faImage} buttonProps={buttonProps.ADD_IMAGE}>
Add Image
<span className="text-[16px]">Add Image</span>
</ToolbarButton>
{/* <ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
Add Video
</ToolbarButton> */}
</PopoverPanel>
</Popover>
</div>
<div className="flex items-center gap-2 pl-2">
<hr className="w-full border-t border-white/15" />
<div className="flex flex-col items-center gap-2">
<div className="flex flex-col items-center gap-2">
{buttonProps.PAINT.active ? (
<div className="relative">
<ColorPicker
color={paintColor.value}
onChange={dispatchUiEvents.toolbarMain.setPaintColor}
faIcon={faPaintbrush}
borderStyle="border-2 bg-primary/30 border-2 border-primary hover:bg-primary/30 text-white"
showBar={false}
staticIconColor="white"
streamChanges
defaultOpen={true}
anchor="right"
anchorGap={12}
closeOnMouseLeave={true}
/>
<div
className="pointer-events-none absolute -bottom-1.5 -right-1.5 h-[18px] w-[18px] rounded-full border border-gray-400"
style={{ backgroundColor: paintColor.value }}
/>
</div>
) : (
<ToolbarButton
icon={faPaintbrush}
buttonProps={buttonProps.PAINT}
tooltip="Paint Brush"
/>
)}
<ToolbarButton
icon={faEraser}
buttonProps={buttonProps.ERASER}
tooltip="Eraser"
/>
</div>
</div>
<hr className="w-full border-t border-white/15" />
<div className="flex flex-col items-center gap-2">
<ToolbarButton
icon={faArrowRotateLeft}
buttonProps={buttonProps.UNDO}
@@ -128,15 +203,11 @@ export const ToolbarMain = ({
buttonProps={buttonProps.REDO}
tooltip="Redo"
/>
<ToolbarButton
{/* <ToolbarButton
icon={faFloppyDisk}
buttonProps={buttonProps.SAVE}
tooltip="Save"
/>
<ButtonPreviewAndRender
buttonPreviewProps={buttonProps.PREVIEW}
buttonRenderProps={buttonProps.DOWNLOAD}
/>
/> */}
</div>
</div>
</>
@@ -7,6 +7,8 @@ export enum ToolbarMainButtonNames {
ADD_CIRCLE = "ADD_CIRCLE",
ADD_TRIANGLE = "ADD_TRIANGLE",
ADD_SQUARE = "ADD_SQUARE",
PAINT = "PAINT",
ERASER = "ERASER",
AI_STYLIZE = "AI_STYLIZE",
UNDO = "UNDO",
REDO = "REDO",
@@ -13,9 +13,6 @@ import {
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
import { ToolbarNodeButtonNames } from "./enums";
import { ToolbarNodeButtonData } from "./data";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment } from "react/jsx-runtime";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
export interface ToolbarNodeProps {
@@ -54,7 +51,7 @@ export const ToolbarNode = ({
className={twMerge(
paperWrapperStyles,
disabled && "pointer-events-none cursor-default bg-ui-border shadow-md",
"flex gap-2 transition",
"flex flex-col gap-2 transition",
)}
>
<ToolbarButton
@@ -1,28 +1,27 @@
import { twMerge } from "tailwind-merge";
import { paperWrapperStyles } from "~/components/styles";
import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
import { ToolbarButton } from "~/components/features/ToolbarButton";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
// import { ToolbarButton } from "~/components/features/ToolbarButton";
export const ToolbarTopLeft = () => {
return (
<div
className={twMerge(
paperWrapperStyles,
"z-20 mt-2 flex h-fit w-fit items-center gap-1 py-2.5 pl-4",
)}
>
<img
src="/brand/Storyteller-Logo-Black.png"
src="/brand/mira-logo.png"
alt="logo"
className="mr-4 h-8"
className="h-[30px] select-none pb-1"
/>
<ToolbarButton icon={faPlus}>
{/* <ToolbarButton icon={faPlus}>
<span className="font-semibold">New Board</span>
</ToolbarButton>
<ToolbarButton icon={faQuestion}>
<span className="font-semibold">Help</span>
</ToolbarButton>
</ToolbarButton> */}
</div>
);
};
@@ -0,0 +1,31 @@
import { faSave } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { Button } from "~/components/ui/Button";
import { toolbarMain } from "~/signals/uiAccess/toolbarMain";
import { dispatchers } from "~/signals/uiEvents/toolbarMain";
import { ToolbarMainButtonNames } from "../ToolbarMain/enum";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
// import { ToolbarButton } from "~/components/features/ToolbarButton";
export const ToolbarTopRight = () => {
const saveButtonState =
toolbarMain.signal.value.buttonStates[ToolbarMainButtonNames.SAVE];
return (
<div
className={twMerge(
"z-20 mt-2 flex h-fit w-fit items-center gap-1 p-4 py-2.5",
)}
>
<Button
icon={faSave}
onClick={dispatchers[ToolbarMainButtonNames.SAVE]}
disabled={saveButtonState?.disabled}
>
Save Image
</Button>
</div>
);
};
+23 -14
View File
@@ -1,26 +1,35 @@
import { twMerge } from "tailwind-merge";
export const paperWrapperStyles =
"rounded-xl border border-ui-border bg-ui-panel p-2 shadow-lg";
"rounded-xl border border-ui-border p-1.5 shadow-lg bg-ui-panel";
export const transitionTimingStyles = "transition duration-150 ease-in-out";
export const dialogBackgroundStyles =
"fixed inset-0 flex w-screen items-center justify-center bg-black/50";
export const toolTipStyles = twMerge(
"relative",
"before:w-0 before:h-0 before:absolute",
"before:left-1/2 before:bottom-full before:mb-[1px] before:-translate-x-1/2 before:z-50",
"before:border-l-8 before:border-l-transparent",
"before:border-r-8 before:border-r-transparent",
"before:border-t-8 before:border-t-white",
"after:absolute after:left-1/2 after:bottom-full after:-translate-x-1/2 after:z-40",
"after:content-[attr(data-tooltip)] after:text-black after:text-nowrap",
"after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1.5 after:mb-2 after:shadow-xl",
"after:hidden before:hidden hover:after:block hover:before:block",
);
export const toolTipStyles = {
base: twMerge(
"relative",
"after:absolute after:z-40",
"after:content-[attr(data-tooltip)] after:text-black after:text-nowrap after:text-sm after:font-medium after:text-white/80 after:glass",
"after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1 after:shadow-xl",
"after:hidden hover:after:block",
),
top: twMerge(
"after:left-1/2 after:bottom-full after:-translate-x-1/2 after:mb-3.5",
),
bottom: twMerge(
"after:left-1/2 after:top-full after:-translate-x-1/2 after:mt-3.5",
),
left: twMerge(
"after:top-1/2 after:right-full after:-translate-y-1/2 after:mr-3.5",
),
right: twMerge(
"after:top-1/2 after:left-full after:-translate-y-1/2 after:ml-3.5",
),
};
export const dialogPanelStyles = twMerge(
"flex max-w-5xl flex-col justify-between gap-4 p-6",
"flex max-w-5xl flex-col justify-between gap-4 p-6 bg-[#1C1C20]",
);
@@ -31,14 +31,14 @@ export const Button = ({
return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600";
}
case "secondary": {
return "bg-gray-600 hover:bg-gray-500 text-white focus-visible:outline-secondary";
return "bg-ui-controls hover:bg-ui-controls/80 text-white focus-visible:outline-secondary";
}
case "action": {
return " bg-action hover:bg-action-500 text-white focus-visible:outline-action";
}
case "primary":
default: {
return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600";
return "bg-primary hover:bg-primary-600 text-white focus-visible:outline-primary-600";
}
}
}
@@ -29,25 +29,25 @@ export const Combobox = ({ options, value, onChange }: ComboBoxInterface) => {
onChange={onChange}
onClose={() => setQuery("")}
>
<div className="relative flex h-10 rounded-md border">
<div className="relative flex h-10 rounded-md border border-ui-border">
<ComboboxInput
className="rounded-md"
className="rounded-lg px-3"
displayValue={(font: string) => font}
onChange={(event) => setQuery(event.target.value)}
/>
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center hover:text-primary">
<FontAwesomeIcon icon={faChevronDown} className="size-4 p-2" />
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center px-3 transition-colors duration-300 hover:text-primary">
<FontAwesomeIcon icon={faChevronDown} className="size-4" />
</ComboboxButton>
</div>
<ComboboxOptions
anchor="bottom"
className="mt-1 w-[var(--input-width)] rounded-lg border empty:invisible"
className="mt-1 w-[var(--input-width)] rounded-lg border border-ui-border empty:invisible"
>
{filteredOptions.map((option, idx) => (
<ComboboxOption
key={idx}
value={option}
className="cursor-pointer bg-white p-2 data-[focus]:bg-primary-200 data-[selected]:font-medium"
className="bg-ui-controls cursor-pointer px-3 py-2 data-[focus]:bg-gray-600 data-[selected]:font-medium"
>
{option}
</ComboboxOption>
+29 -12
View File
@@ -12,6 +12,7 @@ interface SliderProps {
showTooltip?: boolean;
tooltipContent?: ReactNode;
suffix?: string;
innerLabel?: string;
}
export const Slider = ({
@@ -24,6 +25,7 @@ export const Slider = ({
showTooltip,
tooltipContent,
suffix,
innerLabel,
}: SliderProps) => {
const [localValue, setLocalValue] = useState(value);
const sliderRef = useRef<HTMLDivElement>(null);
@@ -88,25 +90,40 @@ export const Slider = ({
<div
ref={sliderRef}
className={twMerge(
"relative h-2.5 w-full cursor-pointer rounded-full bg-gray-500",
"glass group relative h-7 w-full cursor-pointer overflow-hidden rounded-lg border border-ui-border",
isDragging && "!bg-ui-controls/90",
className,
)}
onMouseDown={handleMouseDown}
>
<div
className="absolute h-2.5 rounded-full bg-primary"
style={{ width: `${percentage}%` }}
></div>
<div
className={twMerge(
"absolute -top-1 h-[18px] w-[18px] -translate-x-1/2 transform rounded-full bg-gray-200 transition-colors",
isDragging ? "bg-white" : "bg-gray-300 hover:bg-white",
"absolute h-7 bg-primary/30 transition-colors duration-300 group-hover:bg-primary/50",
isDragging && "!bg-primary/50",
)}
style={{ left: `${percentage}%` }}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
style={{ width: `${percentage}%` }}
>
{innerLabel && (
<span
className={twMerge(
"absolute top-1/2 ml-2.5 -translate-y-1/2 text-nowrap text-sm font-medium text-white/60 transition-colors duration-300 group-hover:text-white",
isDragging && "!text-white",
)}
>
{innerLabel}
</span>
)}
<div
className={twMerge(
"absolute right-0 top-1/2 mr-1.5 h-3.5 w-0.5 -translate-y-1/2",
isDragging ? "bg-white" : "bg-white/50",
)}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</div>
{showTooltip && (
<Transition
as={"div"}
@@ -15,7 +15,7 @@ const resizeStyles = {
horizontal: "resize-x",
vertical: "resize-y",
undefined: "",
}
};
export interface TextareaInterface
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
@@ -27,7 +27,6 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
{ className, label, resize = "vertical", id, ...rest }: TextareaInterface,
ref,
) => {
const resizeStyle = resizeStyles[resize];
return (
@@ -38,17 +37,16 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
ref={ref}
id={id ? id : label ? kebabCase(label) : undefined}
className={twMerge(
"border-ui-panel-border bg-ui-controls rounded-lg border px-3 py-2",
"rounded-lg border border-ui-border px-4 py-2.5 text-white outline-none focus:outline-none",
className,
resizeStyle,
)}
style={{
outline: "2px solid transparent",
transition: "outline-color 0.15s ease-in-out",
}}
onFocus={(e) => {
// disableHotkeyInput(DomLevels.INPUT);
e.currentTarget.style.outlineColor = "#e66462";
e.currentTarget.style.outlineColor = "#2d81ff";
}}
onBlur={(e) => {
// enableHotkeyInput(DomLevels.INPUT);
@@ -1,6 +1,6 @@
import {
faAlignCenter,
faAlignJustify,
// faAlignJustify,
faAlignLeft,
faAlignRight,
} from "@fortawesome/pro-solid-svg-icons";
@@ -1,6 +1,10 @@
import { ChangeEvent, useState } from "react";
import { ChangeEvent, useState, useEffect } from "react";
import { twMerge } from "tailwind-merge";
import { faFont, faHashtag, IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import {
faFont,
faHashtag,
IconDefinition,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { HexColorPicker } from "react-colorful";
@@ -13,15 +17,30 @@ export const ColorPicker = ({
color: prevColor,
onChange,
faIcon = faFont,
borderStyle = "border",
borderStyle,
showBar = true,
anchor = "bottom",
anchorGap = 0,
fillBg = false,
staticIconColor,
streamChanges = false,
defaultOpen = false,
closeOnMouseLeave = false,
}: {
color: string;
onChange: (newColor: string) => void;
faIcon?: IconDefinition;
borderStyle?: string;
showBar?: boolean;
anchor?: "top" | "right" | "bottom" | "left";
anchorGap?: number;
fillBg?: boolean;
staticIconColor?: string;
streamChanges?: boolean;
defaultOpen?: boolean;
closeOnMouseLeave?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
const [{ currColor, textInput }, setStates] = useState<{
currColor: string;
textInput: string;
@@ -30,13 +49,18 @@ export const ColorPicker = ({
textInput: prevColor.substring(1),
});
const handleSelectColor = (newColor: string) => {
setStates({ currColor: newColor, textInput: newColor.substring(1) });
};
const isHexColorCode = (color: string) => {
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
return hexColorRegex.test(color);
useEffect(() => {
setIsOpen(defaultOpen);
}, [defaultOpen]);
const handleMouseLeave = (close: () => void) => {
if (closeOnMouseLeave) {
onChange(currColor);
setIsOpen(false);
close();
}
};
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
if (isHexColorCode(e.target.value)) {
setStates({
@@ -51,58 +75,100 @@ export const ColorPicker = ({
}
};
const isHexColorCode = (color: string) => {
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
return hexColorRegex.test(color);
};
return (
<Popover className="relative">
<PopoverButton className={twMerge(
"flex size-10 flex-col items-center gap-1 rounded-md p-2",
borderStyle
)}>
<FontAwesomeIcon icon={faIcon} color={prevColor} />
{showBar && <span className="h-1 w-full" style={{ backgroundColor: prevColor }} />}
</PopoverButton>
<PopoverPanel
anchor="bottom"
className={twMerge(
paperWrapperStyles,
"flex flex-col items-center gap-2",
)}
>
{({ close }) => {
return (
<>
<HexColorPicker color={currColor} onChange={handleSelectColor} />
<Input
style={{ width: "198px" }}
icon={faHashtag}
value={textInput}
onChange={handleTextInput}
{({ open, close }) => (
<>
<PopoverButton
className={twMerge(
"flex size-10 items-center gap-1 rounded-lg bg-ui-controls p-2",
borderStyle,
showBar ? "flex-col" : "justify-center",
)}
style={fillBg ? { backgroundColor: prevColor } : {}}
onMouseEnter={() => defaultOpen && setIsOpen(true)}
>
<FontAwesomeIcon
icon={faIcon}
color={staticIconColor ?? (fillBg ? undefined : prevColor)}
/>
{showBar && (
<span
className="h-1 w-full"
style={{ backgroundColor: prevColor }}
/>
<div className="flex w-full justify-center gap-2">
<Button
variant="secondary"
onClick={() => {
)}
</PopoverButton>
{(open || isOpen) && (
<div onMouseLeave={() => handleMouseLeave(close)}>
<PopoverPanel
anchor={anchor}
className={twMerge(
paperWrapperStyles,
"flex flex-col items-center gap-2 overflow-hidden",
)}
style={
{
"--anchor-gap": `${anchorGap}px`,
} as React.CSSProperties
}
static
>
<HexColorPicker
className="overflow-hidden"
color={currColor}
onChange={(color) => {
setStates({
currColor: prevColor,
textInput: prevColor.substring(1),
currColor: color,
textInput: color.substring(1),
});
close();
if (streamChanges) {
onChange(color);
}
}}
>
Cancel
</Button>
<Button
onClick={() => {
onChange(currColor);
close();
}}
>
OK
</Button>
</div>
</>
);
}}
</PopoverPanel>
/>
<Input
style={{ width: "198px" }}
icon={faHashtag}
value={textInput}
onChange={handleTextInput}
/>
{!closeOnMouseLeave && (
<div className="flex w-full justify-center gap-2">
<Button
variant="secondary"
onClick={() => {
setStates({
currColor: prevColor,
textInput: prevColor.substring(1),
});
setIsOpen(false);
close();
}}
>
Cancel
</Button>
<Button
onClick={() => {
onChange(currColor);
setIsOpen(false);
close();
}}
>
OK
</Button>
</div>
)}
</PopoverPanel>
</div>
)}
</>
)}
</Popover>
);
};
@@ -151,7 +151,11 @@ export const TextEditor = ({
onChangeTextDecoration={onChangeTextDecoration}
/>
</div>
<Textarea {...unionedTextAreaProps} ref={textAreaRef} />
<Textarea
className="bg-white"
{...unionedTextAreaProps}
ref={textAreaRef}
/>
</div>
);
};
@@ -1,4 +1,4 @@
export const buttonGroupButton =
"size-10 rounded-none bg-white text-black hover:bg-gray-200 data-[selected=true]:text-primary-400 disabled:hover:bg-transparent disabled:text-gray-400";
"size-10 rounded-none bg-ui-controls text-white/80 hover:bg-white/30 data-[selected=true]:text-white data-[selected=true]:bg-primary/30 disabled:hover:bg-transparent disabled:text-gray-400 border border-ui-border";
export const buttonGroupGrouper =
"flex divide-x overflow-hidden rounded-md border";
"flex divide-x overflow-hidden rounded-md border border-ui-border";
@@ -7,15 +7,18 @@ export const Tooltip = ({
tip,
children,
forceShow,
position = "top",
}: {
tip: string;
children: ReactElement<any, string | JSXElementConstructor<any>>;
forceShow?: boolean;
position?: "top" | "bottom" | "left" | "right";
}) => {
const clonedChildren = cloneElement(children, {
"data-tooltip": tip,
className: twMerge(
toolTipStyles,
toolTipStyles.base,
toolTipStyles[position],
forceShow && "after:block before:block",
children.props.className,
),
@@ -1,5 +1,6 @@
import { signal } from "@preact/signals-react";
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
import { isToolbarMode } from "~/signals/uiEvents/toolbarMain/toolbarMain";
interface ToolbarMainSignalInterface {
disabled: boolean;
@@ -42,6 +43,17 @@ export const toolbarMain = {
},
};
},
setMode(mode: ToolbarMainButtonNames) {
if (!isToolbarMode(mode)) {
return;
}
this.changeButtonState(ToolbarMainButtonNames.SELECT, { active: false });
this.changeButtonState(ToolbarMainButtonNames.ERASER, { active: false });
this.changeButtonState(ToolbarMainButtonNames.PAINT, { active: false });
this.changeButtonState(mode, { active: true });
}
};
function initButtonStates() {
@@ -3,11 +3,14 @@ import {
loadingBarRetryDispatch,
loadingBarRetryEventHandler,
} from "./loadingBar";
import { onPaintColorChanged, setPaintColor } from "./paintMode";
export const dispatchers = {
...buttonDispatchers,
loadingBarRetry: loadingBarRetryDispatch,
setPaintColor,
};
export const eventsHandlers = {
...buttonEventsHandlers,
loadingBarRetry: loadingBarRetryEventHandler,
onPaintColorChanged
};
@@ -0,0 +1,19 @@
// Prompt settings
// - Prompt text
import { effect, signal } from "@preact/signals-react";
// - Prompt strength
export const DEFAULT_PAINT_COLOR: string = "#ff0000";
export const paintColor = signal<string>(DEFAULT_PAINT_COLOR);
export const setPaintColor = (data: string) => {
paintColor.value = data;
};
export const onPaintColorChanged = (callback: (data: string) => void) => {
effect(() => {
if (paintColor.value) {
callback(paintColor.value);
}
});
};
@@ -2,6 +2,7 @@ import { MouseEventHandler } from "react";
import { signal, effect, Signal } from "@preact/signals-react";
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
import { AppModes } from "~/KonvaApp/constants";
const events = Object.values(ToolbarMainButtonNames).reduce(
(acc, buttonName) => {
@@ -25,22 +26,35 @@ const effectsCleanups = Object.values(ToolbarMainButtonNames).reduce(
[key in ToolbarMainButtonNames]: (() => void) | undefined;
},
);
export const isToolbarMode = (buttonName: ToolbarMainButtonNames) => {
return appModeMap[buttonName] !== undefined;
}
export const appModeMap: Record<string, AppModes> = {
[ToolbarMainButtonNames.SELECT]: AppModes.SELECT,
[ToolbarMainButtonNames.ERASER]: AppModes.ERASER,
[ToolbarMainButtonNames.PAINT]: AppModes.PAINT,
}
const defaultHandler = (buttonName: ToolbarMainButtonNames, callback: MouseEventHandler<HTMLButtonElement>) => {
if (effectsCleanups[buttonName]) {
effectsCleanups[buttonName]();
}
effectsCleanups[buttonName] = effect(() => {
if (events[buttonName].value) {
callback(events[buttonName].value);
events[buttonName].value = undefined;
}
});
}
export const buttonEventsHandlers = Object.values(
ToolbarMainButtonNames,
).reduce(
(acc, buttonName) => {
acc[buttonName] = {
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
if (effectsCleanups[buttonName]) {
effectsCleanups[buttonName]();
}
effectsCleanups[buttonName] = effect(() => {
if (events[buttonName].value) {
callback(events[buttonName].value);
events[buttonName].value = undefined;
}
});
},
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => { defaultHandler(buttonName, callback) },
};
return acc;
},
+5 -4
View File
@@ -44,14 +44,15 @@ export default {
info: colors.sky[500],
// brand colors
primary: storytellerColors.sunglo,
primary: storytellerColors.azureRadiance,
secondary: storytellerColors.gunpowder,
tertiary: storytellerColors.aquamarineBlue,
ui: {
background: colors.zinc[100],
panel: colors.white,
border: colors.zinc[200],
background: "#101014",
panel: "#3E3E41",
border: "#FFFFFF0D",
controls: "#3E3E41",
},
},
backgroundImage: {
+14 -14
View File
@@ -1,19 +1,19 @@
export const storytellerColors = {
sunglo: {
//sunglo
DEFAULT: "#e66462", //=500
50: "#fdf3f3",
100: "#fce4e4",
200: "#facfce",
300: "#f6acab",
400: "#ef7c7a",
500: "#e66462",
600: "#cf3533",
700: "#ae2927",
800: "#902624",
900: "#782524",
950: "#410f0e",
azureRadiance: {
DEFAULT: "#2d81ff", //=500
50: "#eef6ff",
100: "#d9ebff",
200: "#bcdcff",
300: "#8ec7ff",
400: "#59a7ff",
500: "#2d81ff",
600: "#1b63f5",
700: "#144ee1",
800: "#173fb6",
900: "#19398f",
950: "#142457",
},
gunpowder: {
DEFAULT: "#39394c", //=950
50: "#f5f6f9",