mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 04:22:51 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b52a925c |
@@ -503,10 +503,6 @@ export class Engine {
|
||||
uiEvents.toolbarMain.onPaintColorChanged((color) => {
|
||||
this.realTimeDrawEngine.paintColor = color;
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.onEraseBrushSizeChanged((size: number) => {
|
||||
this.realTimeDrawEngine.paintBrushSize = size;
|
||||
});
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
|
||||
@@ -60,7 +60,8 @@ export class RealTimeDrawEngine {
|
||||
// paint Brush Size
|
||||
// has to exit out of paint mode when shape or image are used.
|
||||
public paintColor: string = "#000000";
|
||||
private brushSize: number = 5; // Default brush size
|
||||
public paintBrushSize: number = 5;
|
||||
public isPaintMode: boolean = false;
|
||||
|
||||
private onDrawCallback?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -169,7 +170,7 @@ export class RealTimeDrawEngine {
|
||||
this.captureCanvas.setZIndex(0);
|
||||
this.previewCanvas.setZIndex(1);
|
||||
// Add mouse events for preview canvas copying
|
||||
this.previewCopyListener();
|
||||
//this.previewCopyListener();
|
||||
|
||||
//this.startServer();
|
||||
}
|
||||
@@ -280,7 +281,7 @@ export class RealTimeDrawEngine {
|
||||
currentLine = new Konva.Line({
|
||||
points: [relativeX, relativeY],
|
||||
stroke: this.paintColor,
|
||||
strokeWidth: this.brushSize * 5,
|
||||
strokeWidth: 5,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
x: this.captureCanvas.x(),
|
||||
@@ -424,82 +425,57 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
|
||||
public previewCopyListener() {
|
||||
// Start of Selection
|
||||
this.previewCanvas.on("mousedown touchstart", () => {
|
||||
if (!this.outputBitmap) {
|
||||
console.log("No preview image to copy");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create draggable preview copy
|
||||
const previewCopy = new Konva.Image({
|
||||
x: this.previewCanvas.x(),
|
||||
y: this.previewCanvas.y(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
image: this.outputBitmap,
|
||||
draggable: true,
|
||||
listening: true,
|
||||
});
|
||||
|
||||
if (this.onPreviewCopyCallback) {
|
||||
this.onPreviewCopyCallback(previewCopy);
|
||||
}
|
||||
previewCopy.startDrag();
|
||||
previewCopy.on("dragend", (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
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(),
|
||||
});
|
||||
e.target.off('dragend');
|
||||
previewCopy.setZIndex(1); // send back after drop.
|
||||
} else {
|
||||
// Start Generation Here
|
||||
previewCopy.destroy();
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
});
|
||||
|
||||
// Start Generation Here
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
const layer = this.mediaLayerRef; // Use the existing media layer
|
||||
this.previewCanvas.on("mousedown touchstart", () => {
|
||||
if (!this.outputBitmap) {
|
||||
console.log("No preview image to copy");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create centered text
|
||||
// Start of Selection
|
||||
const centeredText = new Konva.Text({
|
||||
text: 'Hold and Drag Over, To Copy',
|
||||
fontSize: 24,
|
||||
fontFamily: 'Arial',
|
||||
fill: 'black',
|
||||
x: this.captureCanvas.x() + this.captureCanvas.width() / 2,
|
||||
y: this.captureCanvas.y() + this.captureCanvas.height() / 2,
|
||||
id: 'copyText',
|
||||
listening: false,
|
||||
});
|
||||
// Calculate and set offsets based on text size to center it
|
||||
centeredText.offsetX(centeredText.width() / 2);
|
||||
centeredText.offsetY(centeredText.height() / 2);
|
||||
// Create draggable preview copy
|
||||
const previewCopy = new Konva.Image({
|
||||
x: this.previewCanvas.x(),
|
||||
y: this.previewCanvas.y(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
image: this.outputBitmap,
|
||||
draggable: true,
|
||||
listening: true,
|
||||
});
|
||||
|
||||
layer.add(centeredText);
|
||||
centeredText.moveToTop();
|
||||
layer.batchDraw();
|
||||
|
||||
// Remove text on mouseup/touchend
|
||||
const removeText = () => {
|
||||
const text = layer.findOne('#copyText');
|
||||
if (text) {
|
||||
text.destroy();
|
||||
layer.batchDraw();
|
||||
}
|
||||
stage.off("mouseup touchend", removeText);
|
||||
};
|
||||
stage.on("mouseup touchend", removeText);
|
||||
});
|
||||
// this.drawingsLayer.add(previewCopy); // Add to drawingsLayer instead of mediaLayerRef
|
||||
// this.drawingsLayer.batchDraw();
|
||||
|
||||
// Invoke the callback with the preview copy
|
||||
if (this.onPreviewCopyCallback) {
|
||||
this.onPreviewCopyCallback(previewCopy);
|
||||
}
|
||||
|
||||
// Start dragging immediately
|
||||
previewCopy.startDrag();
|
||||
|
||||
// Handle drag events
|
||||
previewCopy.on("dragmove", () => {
|
||||
// this.drawingsLayer.draw();
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
//this.drawingsLayer.batchDraw();
|
||||
} else {
|
||||
// Remove if dropped outside capture area
|
||||
//previewCopy.destroy();
|
||||
//this.drawingsLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public findImageNodeById(
|
||||
@@ -900,13 +876,4 @@ export class RealTimeDrawEngine {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add getter/setter for brush size
|
||||
public set paintBrushSize(size: number) {
|
||||
this.brushSize = size;
|
||||
}
|
||||
|
||||
public get paintBrushSize(): number {
|
||||
return this.brushSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Button } from "~/components/ui/Button";
|
||||
import { useState } from "react";
|
||||
import { BaseDialog } from "~/components/ui/BaseDialog";
|
||||
import { DialogTitle } from "@headlessui/react";
|
||||
import { LoadingBar, LoadingBarStatus } from "~/components/ui";
|
||||
|
||||
interface ModelButtonProps {
|
||||
iconSrc: string;
|
||||
@@ -53,9 +52,8 @@ const ModelButton = ({
|
||||
|
||||
export const ToolbarTopCenter = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Model selection buttons (only shown in dev mode for now) */}
|
||||
{import.meta.env.DEV && (
|
||||
<>
|
||||
{import.meta.env.DEV && ( // only show in dev mode for now
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-2")}>
|
||||
<ModelButton
|
||||
iconSrc="/resources/icons/model.png"
|
||||
@@ -86,40 +84,6 @@ export const ToolbarTopCenter = () => {
|
||||
</ModelButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading models indicator (only shown in dev mode for now) */}
|
||||
{import.meta.env.DEV && (
|
||||
<div className="flex min-w-[328px] flex-col items-center">
|
||||
<LoadingBar progress={70} status={LoadingBarStatus.LOADING} />
|
||||
<div className="glass flex w-fit items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium opacity-60">
|
||||
<svg
|
||||
className="h-3 w-3 animate-spin text-white/40"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path
|
||||
d="M32 3C35.8083 3 39.5794 3.75011 43.0978 5.20749C46.6163 6.66488 49.8132 8.80101 52.5061 11.4939C55.199 14.1868 57.3351 17.3837 58.7925 20.9022C60.2499 24.4206 61 28.1917 61 32C61 35.8083 60.2499 39.5794 58.7925 43.0978C57.3351 46.6163 55.199 49.8132 52.5061 52.5061C49.8132 55.199 46.6163 57.3351 43.0978 58.7925C39.5794 60.2499 35.8083 61 32 61C28.1917 61 24.4206 60.2499 20.9022 58.7925C17.3837 57.3351 14.1868 55.199 11.4939 52.5061C8.801 49.8132 6.66487 46.6163 5.20749 43.0978C3.7501 39.5794 3 35.8083 3 32C3 28.1917 3.75011 24.4206 5.2075 20.9022C6.66489 17.3837 8.80101 14.1868 11.4939 11.4939C14.1868 8.80099 17.3838 6.66487 20.9022 5.20749C24.4206 3.7501 28.1917 3 32 3L32 3Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M32 3C36.5778 3 41.0906 4.08374 45.1692 6.16256C49.2477 8.24138 52.7762 11.2562 55.466 14.9605C58.1558 18.6647 59.9304 22.9531 60.6448 27.4748C61.3591 31.9965 60.9928 36.6232 59.5759 40.9762"
|
||||
stroke="currentColor"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className="text-white/90"
|
||||
></path>
|
||||
</svg>
|
||||
Downloading models - 70%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { MouseEvent, MouseEventHandler, useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { EyeDropper } from "react-eyedrop";
|
||||
import { Button } from "./Button";
|
||||
import { faEyedropper } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ColorPickerHTMLAttributes = Omit<React.HTMLAttributes<HTMLDivElement>, "color" | "onChange" | "onChangeCapture">;
|
||||
interface ColorPickerBaseProps<T extends string> extends ColorPickerHTMLAttributes {
|
||||
color: T;
|
||||
onChange: (newColor: T) => void;
|
||||
}
|
||||
|
||||
export const HexEyedropPicker = ({
|
||||
color,
|
||||
onDropperChange,
|
||||
dropperClassName,
|
||||
pickerClassName,
|
||||
onPickerChange,
|
||||
Picker
|
||||
}: {
|
||||
color: string;
|
||||
onDropperChange?: (newColor: string) => void;
|
||||
dropperClassName?: string;
|
||||
pickerClassName?: string;
|
||||
onPickerChange: (newColor: string) => void;
|
||||
Picker: (props: Partial<ColorPickerBaseProps<string>>) => JSX.Element;
|
||||
}) => {
|
||||
|
||||
const handleDropperClick = () => {
|
||||
if (!('EyeDropper' in window)) {
|
||||
// NOTE: This shouldn't happen cuz we're using Chromium
|
||||
console.error("EyeDropper not found in window");
|
||||
return;
|
||||
}
|
||||
|
||||
const startEyeDrop = async (): Promise<string | null> => {
|
||||
// @ts-ignore-next-line
|
||||
const dropper = new window.EyeDropper();
|
||||
|
||||
try {
|
||||
const result = await dropper.open();
|
||||
console.log(result);
|
||||
return result.sRGBHex;
|
||||
} catch (err) {
|
||||
// User cancelled the picker
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
startEyeDrop().then((newColor) => {
|
||||
if (newColor) {
|
||||
onDropperChange?.(newColor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleOnMouseDown = () => {
|
||||
setIsDragging(true);
|
||||
}
|
||||
const handleOnMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"relative"}>
|
||||
<Picker
|
||||
className={pickerClassName}
|
||||
color={color}
|
||||
onChange={onPickerChange}
|
||||
onMouseDown={handleOnMouseDown}
|
||||
onMouseUp={handleOnMouseUp}
|
||||
/>
|
||||
<Button
|
||||
className={twMerge([
|
||||
"rounded-full m-1 size-8 text-ui-controls bg-transparent hover:bg-black/70 hover:text-white z-20 shadow-none hover:shadow-xl duration-200",
|
||||
dropperClassName,
|
||||
"absolute left-0 top-0",
|
||||
isDragging ? "opacity-0 -z-10" : ""
|
||||
])}
|
||||
icon={faEyedropper}
|
||||
onClick={handleDropperClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -35,14 +35,17 @@ export const LoadingBar = ({
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"h-1.5 w-full rounded-full",
|
||||
"h-2.5 w-full rounded-full",
|
||||
status && status === LoadingBarStatus.IDLE
|
||||
? "bg-gray-50"
|
||||
: "pointer-events-none border-0 bg-gray-600",
|
||||
? "border-1 bg-gray-50 ring-1 ring-inset ring-gray-200"
|
||||
: "bg-gray-200",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge("h-full rounded-full bg-primary/80")}
|
||||
className={twMerge(
|
||||
"h-2.5 rounded-full bg-primary-500",
|
||||
status === LoadingBarStatus.LOADING && "animate-pulse",
|
||||
)}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { twMerge } from "tailwind-merge";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { HexEyedropPicker } from "../HexEyedropPicker";
|
||||
|
||||
export const ColorPicker = ({
|
||||
color: prevColor,
|
||||
@@ -83,16 +82,6 @@ export const ColorPicker = ({
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
@@ -133,12 +122,18 @@ export const ColorPicker = ({
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
pickerClassName="overflow-hidden"
|
||||
<HexAlphaColorPicker
|
||||
className="overflow-hidden"
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexAlphaColorPicker}
|
||||
onChange={(color) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
import { EyeDropper } from "react-eyedrop";
|
||||
import { HexEyedropPicker } from "../HexEyedropPicker";
|
||||
|
||||
export const BackgroundMenu = ({
|
||||
color: prevColor,
|
||||
@@ -90,16 +88,6 @@ export const BackgroundMenu = ({
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}
|
||||
|
||||
// Rerender the component when these signals change:
|
||||
useSignals();
|
||||
|
||||
@@ -142,12 +130,18 @@ export const BackgroundMenu = ({
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
pickerClassName="overflow-hidden"
|
||||
<HexColorPicker
|
||||
className="overflow-hidden"
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexColorPicker}
|
||||
onChange={(color) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, paintBrushSize, setPaintBrushSize } fro
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
import { HexEyedropPicker } from "../HexEyedropPicker";
|
||||
|
||||
export const PaintModeMenu = ({
|
||||
color: prevColor,
|
||||
@@ -84,16 +83,6 @@ export const PaintModeMenu = ({
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}
|
||||
|
||||
// Rerender the component when these signals change:
|
||||
useSignals();
|
||||
const brushSize = paintBrushSize.value;
|
||||
@@ -137,12 +126,18 @@ export const PaintModeMenu = ({
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
pickerClassName="overflow-hidden"
|
||||
<HexAlphaColorPicker
|
||||
className="overflow-hidden"
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexAlphaColorPicker}
|
||||
onChange={(color) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
|
||||
Reference in New Issue
Block a user