working on canvas and widget
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"fabric": "^6.1.0",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"re-resizable": "^6.9.17",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-query": "^3.39.3",
|
||||
@@ -16756,6 +16757,15 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/re-resizable": {
|
||||
"version": "6.9.17",
|
||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.17.tgz",
|
||||
"integrity": "sha512-OBqd1BwVXpEJJn/yYROG+CbeqIDBWIp6wathlpB0kzZWWZIY1gPTsgK2yJEui5hOvkCdC2mcexF2V3DZVfLq2g==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"fabric": "^6.1.0",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"re-resizable": "^6.9.17",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-query": "^3.39.3",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/ic
|
||||
import Sidebar from './sidebar/sidebar'
|
||||
import WidgetsContainer from './sidebar/widgetsContainer'
|
||||
import UploadsContainer from './sidebar/uploadsContainer'
|
||||
import Canvas from './canvas/mainClass'
|
||||
import Canvas from './canvas/canvas'
|
||||
|
||||
function App() {
|
||||
|
||||
|
||||
269
src/canvas/canvas.js
Normal file
269
src/canvas/canvas.js
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from "react"
|
||||
import * as fabric from 'fabric'
|
||||
import { FullscreenOutlined, ReloadOutlined } from "@ant-design/icons"
|
||||
import { Button, Tooltip } from "antd"
|
||||
|
||||
import Widget from "./widgets/base"
|
||||
import Cursor from "./constants/cursor"
|
||||
|
||||
|
||||
class Canvas extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.canvasRef = React.createRef()
|
||||
this.canvasContainerRef = React.createRef()
|
||||
|
||||
/**
|
||||
* @type {Widget[]}
|
||||
*/
|
||||
this.widgets = []
|
||||
|
||||
this.modes = {
|
||||
DEFAULT: '',
|
||||
PAN: 'pan',
|
||||
}
|
||||
this.currentMode = this.modes.DEFAULT
|
||||
|
||||
this.mousePressed = false
|
||||
this.mousePos = {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
|
||||
this.state = {
|
||||
widgets: [],
|
||||
zoom: 1,
|
||||
isPanning: false,
|
||||
currentTranslate: { x: 0, y: 0 },
|
||||
}
|
||||
|
||||
this.resetTransforms = this.resetTransforms.bind(this)
|
||||
|
||||
// this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initEvents()
|
||||
|
||||
// this.widgets.push(new Widget())
|
||||
|
||||
this.setState({widgets: [new Widget()]})
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
}
|
||||
|
||||
mouseDownEvent(event){
|
||||
|
||||
this.mousePressed = true
|
||||
|
||||
this.mousePos.x = event.e.clientX
|
||||
this.mousePos.y = event.e.clientY
|
||||
|
||||
}
|
||||
|
||||
mouseMoveEvent(event){
|
||||
|
||||
}
|
||||
|
||||
mouseUpEvent(event){
|
||||
this.mousePressed = false
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {import("./widgets/base").Widget[]}
|
||||
*/
|
||||
getWidgets(){
|
||||
|
||||
return this.state.widgets
|
||||
}
|
||||
|
||||
/**
|
||||
* returns list of active objects on the canvas
|
||||
* @returns Widget[]
|
||||
*/
|
||||
getActiveObjects(){
|
||||
|
||||
return this.getWidgets().filter((widget) => {
|
||||
return widget.isSelected
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
initEvents(){
|
||||
|
||||
this.canvasContainerRef.current.addEventListener("mousedown", (event) => {
|
||||
this.mousePressed = true
|
||||
this.mousePos = { x: event.clientX, y: event.clientY }
|
||||
|
||||
if (this.state.widgets.length > 0){
|
||||
|
||||
this.currentMode = this.modes.PAN
|
||||
this.setCursor(Cursor.GRAB)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
this.canvasContainerRef.current.addEventListener("mouseup", () => {
|
||||
this.mousePressed = false
|
||||
this.currentMode = this.modes.DEFAULT
|
||||
this.setCursor(Cursor.DEFAULT)
|
||||
})
|
||||
|
||||
this.canvasContainerRef.current.addEventListener("mousemove", (event) => {
|
||||
// console.log("event: ", event)
|
||||
if (this.mousePressed && this.currentMode === this.modes.PAN) {
|
||||
const deltaX = event.clientX - this.mousePos.x
|
||||
const deltaY = event.clientY - this.mousePos.y
|
||||
|
||||
this.setState(prevState => ({
|
||||
currentTranslate: {
|
||||
x: prevState.currentTranslate.x + deltaX,
|
||||
y: prevState.currentTranslate.y + deltaY,
|
||||
}
|
||||
}), this.applyTransform)
|
||||
|
||||
this.mousePos = { x: event.clientX, y: event.clientY }
|
||||
|
||||
this.setCursor(Cursor.GRAB)
|
||||
}
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
this.canvasContainerRef.current.addEventListener("selection:created", () => {
|
||||
console.log("selected")
|
||||
this.currentMode = this.modes.DEFAULT
|
||||
})
|
||||
|
||||
this.canvasContainerRef.current.addEventListener('wheel', (event) => {
|
||||
this.wheelZoom(event)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
applyTransform(){
|
||||
const { currentTranslate, zoom } = this.state
|
||||
this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})`
|
||||
}
|
||||
|
||||
wheelZoom(event){
|
||||
let delta = event.deltaY
|
||||
let zoom = this.state.zoom * 0.999 ** delta
|
||||
this.setZoom(zoom, {x: event.offsetX, y: event.offsetY})
|
||||
}
|
||||
|
||||
/**
|
||||
* fits the canvas size to fit the widgets bounding box
|
||||
*/
|
||||
fitCanvasToBoundingBox(){
|
||||
// this.canvasRef.current.style.width = this.canvasContainerRef.current.clientWidth
|
||||
// this.canvasRef.current.style.height = this.canvasContainerRef.current.clientHeight
|
||||
}
|
||||
|
||||
setCursor(cursor){
|
||||
this.canvasContainerRef.current.style.cursor = cursor
|
||||
}
|
||||
|
||||
setZoom(zoom, pos={x:0, y:0}){
|
||||
|
||||
const { currentTranslate } = this.state
|
||||
|
||||
// Calculate the new translation to zoom into the mouse position
|
||||
const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x)
|
||||
const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y)
|
||||
|
||||
const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom)
|
||||
const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom)
|
||||
|
||||
this.setState({
|
||||
zoom: zoom,
|
||||
currentTranslate: {
|
||||
x: newTranslateX,
|
||||
y: newTranslateY
|
||||
}
|
||||
}, this.applyTransform)
|
||||
|
||||
}
|
||||
|
||||
getCanvasObjectsBoundingBox(padding = 0) {
|
||||
const objects = this.fabricCanvas.getObjects()
|
||||
if (objects.length === 0) {
|
||||
return { left: 0, top: 0, width: this.fabricCanvas.width, height: this.fabricCanvas.height }
|
||||
}
|
||||
|
||||
const boundingBox = objects.reduce((acc, obj) => {
|
||||
const objBoundingBox = obj.getBoundingRect(true)
|
||||
acc.left = Math.min(acc.left, objBoundingBox.left)
|
||||
acc.top = Math.min(acc.top, objBoundingBox.top)
|
||||
acc.right = Math.max(acc.right, objBoundingBox.left + objBoundingBox.width)
|
||||
acc.bottom = Math.max(acc.bottom, objBoundingBox.top + objBoundingBox.height)
|
||||
return acc
|
||||
}, {
|
||||
left: Infinity,
|
||||
top: Infinity,
|
||||
right: -Infinity,
|
||||
bottom: -Infinity
|
||||
})
|
||||
|
||||
// Adding padding
|
||||
boundingBox.left -= padding
|
||||
boundingBox.top -= padding
|
||||
boundingBox.right += padding
|
||||
boundingBox.bottom += padding
|
||||
|
||||
return {
|
||||
left: boundingBox.left,
|
||||
top: boundingBox.top,
|
||||
width: boundingBox.right - boundingBox.left,
|
||||
height: boundingBox.bottom - boundingBox.top
|
||||
}
|
||||
}
|
||||
|
||||
resetTransforms() {
|
||||
this.setState({
|
||||
zoom: 1,
|
||||
currentTranslate: { x: 0, y: 0 }
|
||||
}, this.applyTransform)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="tw-relative tw-flex tw-w-full tw-h-full tw-max-h-[100vh]">
|
||||
|
||||
<div className="tw-absolute tw-p-2 tw-bg-white tw-z-10 tw-min-w-[100px] tw-h-[50px] tw-gap-2
|
||||
tw-top-4 tw-place-items-center tw-right-4 tw-shadow-md tw-rounded-md tw-flex">
|
||||
|
||||
<Tooltip title="Reset viewport">
|
||||
<Button icon={<ReloadOutlined />} onClick={this.resetTransforms} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="tw-w-full tw-relative tw-h-full tw-bg-red-300 tw-overflow-hidden"
|
||||
ref={this.canvasContainerRef}
|
||||
style={{transition: " transform 0.3s ease-in-out"}}
|
||||
>
|
||||
<div data-canvas className="tw-w-full tw-absolute tw-top-0 tw-h-full tw-bg-green-300"
|
||||
ref={this.canvasRef}>
|
||||
<div className="tw-relative tw-w-full tw-h-full">
|
||||
{
|
||||
this.state.widgets.map((wid, index) => {
|
||||
return <React.Fragment key={index}>{React.cloneElement(wid, {ref:wid.elementRef})}</React.Fragment>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Canvas
|
||||
16
src/canvas/constants/cursor.js
Normal file
16
src/canvas/constants/cursor.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
const Cursor = {
|
||||
POINTER: 'pointer',
|
||||
DEFAULT: 'default',
|
||||
CROSSHAIR: 'crosshair',
|
||||
EW_RESIZE: 'e-resize', // east resize
|
||||
NS_RESIZE: 'n-resize', // east resize
|
||||
NW_RESIZE: 'nw-resize', // east resize
|
||||
SE_RESIZE: 'se-resize',
|
||||
SW_RESIZE: 'sw-resize',
|
||||
GRAB: 'grab',
|
||||
GRABBING: 'grabbing',
|
||||
}
|
||||
|
||||
|
||||
export default Cursor
|
||||
7
src/canvas/constants/layouts.js
Normal file
7
src/canvas/constants/layouts.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const Layouts = {
|
||||
PACK: "flex",
|
||||
GRID: "grid",
|
||||
PLACE: "absolute"
|
||||
}
|
||||
|
||||
export default Layouts
|
||||
8
src/canvas/constants/tools.js
Normal file
8
src/canvas/constants/tools.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
const Tools = {
|
||||
COLOR_PICKER: "color_picker",
|
||||
EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown
|
||||
}
|
||||
|
||||
|
||||
export default Tools
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import * as fabric from 'fabric'
|
||||
|
||||
/**
|
||||
* NOTE: Not in use
|
||||
*/
|
||||
function FabricJSCanvas({ canvasOptions, className = '', onCanvasContextUpdate }) {
|
||||
|
||||
const canvasRef = useRef(null)
|
||||
|
||||
const fabricCanvasRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const options = {}
|
||||
let canvas = null
|
||||
if (canvasRef.current) {
|
||||
canvas = new fabric.Canvas(canvasRef.current, options)
|
||||
const parent = canvasRef.current.parentNode.parentNode
|
||||
canvas.setDimensions({ width: parent.clientWidth, height: parent.clientHeight })
|
||||
canvas.calcOffset()
|
||||
|
||||
fabricCanvasRef.current = canvas
|
||||
canvasRef.current.parentNode.style.width = "100%"
|
||||
canvasRef.current.parentNode.style.height = "100%"
|
||||
|
||||
console.log("Parent: ", canvasRef.current.parentNode.parentNode)
|
||||
|
||||
canvasRef.current.parentNode.parentNode.addEventListener("resize", updateCanvasDimensions)
|
||||
window.addEventListener("resize", updateCanvasDimensions)
|
||||
|
||||
// make the fabric.Canvas instance available to your app
|
||||
if (onCanvasContextUpdate)
|
||||
onCanvasContextUpdate(canvas)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateCanvasDimensions)
|
||||
canvasRef.current.parentNode.parentNode.removeEventListener("resize", updateCanvasDimensions)
|
||||
|
||||
if (onCanvasContextUpdate)
|
||||
onCanvasContextUpdate(null)
|
||||
|
||||
canvas.dispose()
|
||||
}
|
||||
}, [canvasRef])
|
||||
|
||||
|
||||
const updateCanvasDimensions = useCallback(() => {
|
||||
if (!canvasRef.current || !fabricCanvasRef.current)
|
||||
return
|
||||
// console.log("Updating canvas")
|
||||
const parent = canvasRef.current.parentNode.parentNode
|
||||
|
||||
fabricCanvasRef.current.setDimensions({ width: parent.clientWidth, height: parent.clientHeight })
|
||||
fabricCanvasRef.current.calcOffset()
|
||||
|
||||
fabricCanvasRef.current.renderAll()
|
||||
|
||||
}, [fabricCanvasRef, canvasRef])
|
||||
|
||||
|
||||
|
||||
return <canvas className={className} ref={canvasRef}/>
|
||||
}
|
||||
|
||||
|
||||
export default FabricJSCanvas
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useCallback, useRef, useEffect, useMemo } from "react"
|
||||
import * as fabric from 'fabric'
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* This is a functional based component of the canvas which was discarded because it makes code
|
||||
* ugly and hard to interface with fabric.js, Fabric 6 has breaking changes such as dispose, is async
|
||||
* which interferes wih useEffect
|
||||
*/
|
||||
|
||||
function Canvas(){
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import { FullscreenOutlined } from "@ant-design/icons"
|
||||
import { Button, Tooltip } from "antd"
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated - Fabric.js cannot be used as its limited to drawing, elements cannot be added to canvas
|
||||
*/
|
||||
|
||||
class Canvas extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
|
||||
178
src/canvas/widgets/base.js
Normal file
178
src/canvas/widgets/base.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import React from "react"
|
||||
import { NotImplementedError } from "../../utils/errors"
|
||||
|
||||
import Tools from "../constants/tools"
|
||||
import Layouts from "../constants/layouts"
|
||||
import Cursor from "../constants/cursor"
|
||||
|
||||
|
||||
function UID(){
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class to be extended
|
||||
*/
|
||||
class Widget extends React.Component{
|
||||
|
||||
constructor(props, _type="widget"){
|
||||
|
||||
super(props)
|
||||
|
||||
// const _type="widget"
|
||||
this.type = _type
|
||||
// this id has to be unique inside the canvas, it will be set automatically and should never be changed
|
||||
this.__id = `${_type}_${UID()}`
|
||||
this._zIndex = 0
|
||||
|
||||
this._selected = false
|
||||
this._disableResize = false
|
||||
this._disableSelection = false
|
||||
|
||||
this.cursor = Cursor.POINTER
|
||||
|
||||
this.icon = "" // antd icon name representing this widget
|
||||
|
||||
this.elementRef = React.createRef()
|
||||
|
||||
this.props = {
|
||||
styling: {
|
||||
backgroundColor: {
|
||||
tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string
|
||||
value: ""
|
||||
},
|
||||
foregroundColor: {
|
||||
tool: Tools.COLOR_PICKER,
|
||||
value: ""
|
||||
},
|
||||
},
|
||||
layout: "show", // enables layout use "hide" to hide layout dropdown, takes the layout from this.layout
|
||||
events: {
|
||||
event1: {
|
||||
tool: Tools.EVENT_HANDLER,
|
||||
value: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.functions = {
|
||||
"load": {"args1": "number", "args2": "string"}
|
||||
}
|
||||
|
||||
|
||||
this.layout = Layouts.PACK
|
||||
this.boundingRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 100,
|
||||
width: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
setComponentAdded(added=true){
|
||||
|
||||
|
||||
// this.elementRef = document.querySelector(`[data-id="${this.__id}"]`)
|
||||
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.elementRef?.addEventListener("click", this.mousePress)
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.elementRef?.removeEventListener("click", this.mousePress)
|
||||
}
|
||||
|
||||
mousePress(event){
|
||||
|
||||
if (!this._disableSelection){
|
||||
this._selected = true
|
||||
|
||||
const widgetSelected = new CustomEvent("selection:created", {
|
||||
detail: {
|
||||
event,
|
||||
id: this.__id,
|
||||
element: this
|
||||
},
|
||||
})
|
||||
console.log("dispatched")
|
||||
document.dispatchEvent(widgetSelected)
|
||||
}
|
||||
}
|
||||
|
||||
select(){
|
||||
this._selected = true
|
||||
}
|
||||
|
||||
deSelect(){
|
||||
this._selected = false
|
||||
}
|
||||
|
||||
getProps(){
|
||||
return this.props
|
||||
}
|
||||
|
||||
getWidgetFunctions(){
|
||||
return this.functions
|
||||
}
|
||||
|
||||
getId(){
|
||||
return this.__id
|
||||
}
|
||||
|
||||
renderContent(){
|
||||
// throw new NotImplementedError("render method has to be implemented")
|
||||
return (
|
||||
<div className="tw-w-full tw-h-full tw-bg-red-400">
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an internal methods don't override
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
render(){
|
||||
|
||||
let style = {
|
||||
cursor: this.cursor,
|
||||
top: "40px",
|
||||
left: "40px",
|
||||
width: this.boundingRect.width,
|
||||
height: this.boundingRect.height
|
||||
}
|
||||
|
||||
let selectionStyle = {
|
||||
x: "-5px",
|
||||
y: "-5px",
|
||||
width: this.boundingRect.width + 5,
|
||||
height: this.boundingRect.height + 5
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div data-id={this.__id} className="tw-relative tw-w-fit tw-h-fit" style={style}
|
||||
>
|
||||
|
||||
{this.render()}
|
||||
<div className="tw-absolute tw-bg-transparent tw-scale-[1.1] tw-opacity-35
|
||||
tw-w-full tw-h-full tw-top-0 tw-border-2 tw-border-solid tw-border-black">
|
||||
|
||||
<div className="">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default Widget
|
||||
8
src/utils/errors.js
Normal file
8
src/utils/errors.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export class NotImplementedError extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user