working on canvas and widget

This commit is contained in:
paul
2024-08-08 16:21:19 +05:30
parent 358282c9f0
commit 35cc57c453
12 changed files with 508 additions and 70 deletions

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View 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

View 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

View File

@@ -0,0 +1,7 @@
const Layouts = {
PACK: "flex",
GRID: "grid",
PLACE: "absolute"
}
export default Layouts

View 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

View File

@@ -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

View File

@@ -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(){

View File

@@ -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
View 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
View File

@@ -0,0 +1,8 @@
export class NotImplementedError extends Error {
constructor(message) {
super(message)
this.message = message
}
}