I am using react with typescript. I use SVG to draw rectangles inside it. I am working on two features, the first one is I have to draw any number of shapes inside the SVG, and the other one is to allow mouse drag option. Now, the problem is when even I am drawing a shape and then drawing another shape in it the first drawn shape is moving and the new shape is drawing.
I want to do if I click and move the shape my drawing rectangle functionality should not work and if I am drawing the rectangle the already drawn shape would not move. this happening because I am using mouseup and mousemove events for both logic and that is why they collapsing. I don't know to separate them.
here is my code:
import "./styles.css";
import React, { useEffect, useRef } from 'react';
interface Iboxes{
id: string,
coordinates:{
x: number
y: number
width: number
height: number
}
}
export default function App() {
const divRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const boxes: Array<Iboxes> = [];
useEffect(() => {
const containerSvg = svgRef.current;
let p:DOMPoint;
let w:number;
let h:number;
if(containerSvg){
const svgPoint = (elem:any, x:number, y:number) => {
const point = containerSvg.createSVGPoint();
point.x = x;
point.y = y;
return point.matrixTransform(elem.getScreenCTM().inverse());
};
containerSvg.addEventListener('mousedown', (event) => {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
const start = svgPoint(containerSvg, event.clientX, event.clientY);
const drawRect = (e:any) => {
p = svgPoint(containerSvg, e.clientX, e.clientY);
w = Math.abs(p.x - start.x);
h = Math.abs(p.y - start.y);
if (p.x > start.x) {
p.x = start.x;
}
if (p.y > start.y) {
p.y = start.y;
}
rect.setAttributeNS(null, 'x', p.x as unknown as string);
rect.setAttributeNS(null, 'y', p.y as unknown as string);
rect.setAttributeNS(null, 'width', w as unknown as string);
rect.setAttributeNS(null, 'height', h as unknown as string);
rect.setAttributeNS(null, 'class', 'svg_rec');
rect.setAttributeNS(null, 'id', 'rec_' + boxes.length as unknown as string)
containerSvg.appendChild(rect);
};
const endDraw = (e:any) => {
containerSvg.removeEventListener('mousemove', drawRect);
containerSvg.removeEventListener('mouseup', endDraw);
boxes.push({id:'rec_' + boxes.length as unknown as string, coordinates:{x: p.x, y: p.y, width: w, height: h}})
let offset:any;
let selectedRect: SVGRectElement | null;
rect.addEventListener('mousedown', startDrag);
rect.addEventListener('mousemove', drag);
rect.addEventListener('mouseup', endDrag);
rect.addEventListener('mouseleave', endDrag);
function startDrag(evt:any) {
selectedRect = rect;
if(selectedRect){
offset = getMousePosition(evt);
if(offset){
let rectX = selectedRect.getAttributeNS(null, 'x');
let rectY = selectedRect.getAttributeNS(null, 'y');
if(rectX && rectY){
offset.x -= parseFloat(rectX);
offset.y -= parseFloat(rectY);
}
}
}
}
function drag(evt:any) {
if(selectedRect){
var coord = getMousePosition(evt);
if(coord && offset){
let x = coord.x - offset.x;
let y = coord.y - offset.y;
if(x && y){
selectedRect.setAttributeNS(null, "x", x as unknown as string);
selectedRect.setAttributeNS(null, "y", y as unknown as string);
}
}
}
}
function endDrag() {
selectedRect = null;
}
function getMousePosition(evt:any) {
var CTM = rect.getScreenCTM();
if(CTM){
return {
x: (evt.clientX - CTM.e) / CTM.a,
y: (evt.clientY - CTM.f) / CTM.d
};
}
}
};
containerSvg.addEventListener('mousemove', drawRect);
containerSvg.addEventListener('mouseup', endDraw);
});
}
},[]);
return (
<div className="App">
<div className='container' ref={divRef}>
<svg id="svg" ref={svgRef}>
</svg>
</div>
</div>
);
}
I also created a sandbox environment to demonstrate my issue:
here is a sandbox link
You just need to add
evt.stopPropagation();
to the function startDrag() on line 79.
function startDrag(evt: any) {
evt.stopPropagation();
selectedRect = rect;
// ...
}
Here's the fixed sandbox
Edit
Because other rects on top may obscure the current dragging rect. One solution is to disable the pointer-events for other rects while dragging one rect:
Add following code to the startDrag function
containerSvg.classList.add("dragging");
selectedRect?.classList.add("target");
and then add following code to the endDrag function
containerSvg.classList.remove("dragging");
selectedRect?.classList.remove("target");
then in your css code, add following rule:
.dragging rect:not(.target) {
pointer-events: none;
}
Checked updated sandbox
Related
I was about to make some rotation programm (worked by mouse click & drag) but the 'removeEventListener' doesn't work.
can u explain me how to work it and why doesn't it work?
And this is my first question in here so if u find any problems about this question, I'll gladly accept it.
<body>
<div class="wrap">
<div class="target">target</div>
</div>
</body>
const html = document.querySelector("html");
const info = document.querySelector(".info");
const target = document.querySelector(".target");
const wrap = document.querySelector(".wrap");
let center = {
x: target.getBoundingClientRect().left + target.clientWidth / 2,
y: target.getBoundingClientRect().top + target.clientHeight / 2,
};
window.addEventListener("resize", () => {
center = {
x: target.getBoundingClientRect().left + target.clientWidth / 2,
y: target.getBoundingClientRect().top + target.clientHeight / 2,
};
});
const rotate = function () {
target.addEventListener("mousemove", (e) => {
const x = center.x - e.clientX;
const y = center.y - e.clientY;
const radian = Math.atan2(y, x);
const degree = ((radian * 180) / Math.PI).toFixed(0);
target.style.transform = "rotate(" + degree + "deg)";
});
};
target.addEventListener("mousedown", rotate, true);
target.addEventListener("mouseup", () => {
target.removeEventListener("mousedown", rotate, false);
});
I've tried to change this part. target -> wrap and removeEventListener's param to another one. But none of those worked
target.addEventListener("mouseup", () => {
target.removeEventListener("mousedown", rotate, false);
});
The capture/ use capture falg must match to remove listener. Since you used true for your mousedown even listener you also must use true to remove event listener:
target.addEventListener("mouseup", () => {
target.removeEventListener("mousedown", rotate, true);
});
See docs for more details: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#matching_event_listeners_for_removal
I'm trying to make a simple graphics program for doodling and such, but I'm having some trouble with the line tools provided by PIXI.Graphics().
I draw lines by using events such as pointerdown, pointermove, etc. Saving the last coordinate and then using moveTo and lineTo to draw a continuous line until you trigger pointerup. When drawing a line however gaps appear in semi-regular intervals.
Those gaps are always in the color used in the beginFill() fill statement. Even if I use multiple colors, they will appear in the line, making it not really "gaps" but wrongly colored lines.
When a long line is drawn via code, the same error does not seem to occur.
I'm quite stumped by the behaviour and it would be nice if you could help me out!
Here is the code I used:
let app;
let graphics;
var isDrawing = false;
var x = 0;
var y = 0;
var count = 0;
window.onload = function(){
var canvasContainer = document.getElementById("areaCanvasContainer");
//Create a Pixi Application
app = new PIXI.Application({
width: 600,
height: 500,
antialias: false,
transparent: false,
resolution: 1,
forceCanvas: false,
backgroundColor: 0xFFFFFF
});
canvasContainer.appendChild(app.view);
graphics = new PIXI.Graphics();
graphics.interactive = true;
graphics.buttonMode = true;
graphics.beginFill(0xFFFFFF);
graphics.drawRect(0, 0, 600, 500);
graphics.endFill();
graphics.lineStyle(3, 0x000000,1.0,0.5,false);
graphics.on("pointerdown", mousedown);
graphics.on("pointermove", mousemove);
graphics.on("pointerup", mouseup);
graphics.on("pointerupoutside", mouseup);
app.stage.addChild(graphics);
}
function mousedown(e) {
x = e.data.global.x;
y = e.data.global.y;
isDrawing = true;
}
function mousemove(e) {
if (isDrawing === true) {
if(Math.abs(x - e.data.global.x) > 2 || Math.abs(y - e.data.global.y ) > 2 ){
drawLine(graphics, x, y, e.data.global.x, e.data.global.y);
x = e.data.global.x;
y = e.data.global.y;
}
}
}
function mouseup(e) {
if (isDrawing === true) {
drawLine(graphics, x, y, e.data.global.x, e.data.global.y);
x = 0;
y = 0;
isDrawing = false;
}
}
function drawLine(graphics, x1, y1, x2, y2) {
if(!(x1 === x2 && y1 === y2)){
graphics.endFill();
graphics.moveTo(x1, y1);
graphics.lineTo(x2, y2);
}
}
Iterating over plotData creates a rect around certain values on a chart based on a condition.
plotData.forEach( (each, index) => {
if ( each.a > each.b ) {
try {
ctx.beginPath();
ctx.fillStyle = 'yellow';
ctx.rect(zone[index].x, zone[index].y, zone[index].width, zone[index].height);
ctx.closePath();
ctx.fill();
}
...
I want to change the color of the rect when the mouse hovers over it. The chart is scrollable and zoomable therefore the rect coordinates change dynamically.
Firstly, outside of this function, somewhere else in the document code, in fact: at the very top of your code, define a variable to keep track of the mouses location, and if its clicked or not:
var mouseInfo = {x:0, y:0, clicked: false};
Now, somewhere else make some event listeners that keep track of that location (sometime after the canvas element has been made), I'm assuming the canvas element is stored in a variable called canvas:
canvas.addEventListener("mousemove", e=> {
mouseInfo.x = e.offsetX;
mouseInfo.y = e.offsetY;
});
next, another 2 to keep track of if the mouse is down or not (doesn't have to be on the canvas element, in fact, better that it should be on the window element):
window.addEventListener("mousedown", e=> {
mouseInfo.clicked = true;
});
window.addEventListener("mouseup", e=> {
mouseInfo.clicked = false;
});
Now make a function to determine if a point is intersecting a rectangle, assuming a rectangle is an object with an x, y, width, height:
function isPointInRet(point, rect) {
return (
point.x > rect.x &&
point.x < rect.x + rect.width &&
point.y > rect.y &&
point.y < rect.y + rect.height
);
}
Now, back in your function that you quoted above:
plotData.forEach( (each, index) => {
if ( each.a > each.b ) {
try {
ctx.beginPath();
ctx.fillStyle = 'yellow';
ctx.rect(zone[index].x, zone[index].y, zone[index].width, zone[index].height);
ctx.closePath();
ctx.fill();
}
So, right before the fillStyle part, change it all to the following:
var x = zone[index].x,
y = zone[index].y,
width = zone[index].width,
height = zone[index].height;
if(isPointInRect(mouseInfo, {x, y, width, height}) {
//perhaps if you want to make a hover color, do that here, just un-comment if you want:
/*ctx.fillStyle = "#ffaaee";*/
if(mouseInfo.clicked) {
ctx.fillStyle = "ff0000"; //red
}
}
ctx.fillStyle = 'yellow';
ctx.rect(x, y, width, height);
...
(warning: untested code);
let me know if that helps, and if it works with the zooming etc.
I'm trying to create an infinite looping canvas based on a main 'grid'. Scaled down fiddle here with the grid in the centre of the viewport.
JS Fiddle here
In the fiddle I have my main grid of coloured squares in the centre, I want them tiled infinitely in all directions. Obviously this isn't realistically possible, so I want to give the illusion of infinite by just redrawing the grids based on the scroll direction.
I found some good articles:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Tilemaps/Square_tilemaps_implementation:_Scrolling_maps
https://gamedev.stackexchange.com/questions/71583/html5-dynamic-canvas-grid-for-scrolling-a-big-map
And the best route seems to be to get the drag direction and then reset camera to that point, so the layers scroll under the main canvas viewport, thus meaning the camera can never reach the edge of the main viewport canvas.
I've worked on adding some event listeners for mouse drags :
Fiddle with mouse events
var bMouseDown = false;
var oPreviousCoords = {
'x': 0,
'y': 0
}
var oDelta;
var oEndCoords;
var newLayerTop;
$(document).on('mousedown', function (oEvent) {
bMouseDown = true;
oPreviousCoords = {
'x': oEvent.pageX,
'y': oEvent.pageY
}
});
$(document).on('mouseup', function (oEvent) {
bMouseDown = false;
oPreviousCoords = {
'x': oEvent.pageX,
'y': oEvent.pageY
}
oEndCoords = oDelta
if(oEndCoords.y < -300){
if(newLayerTop){
newLayerTop.destroy();
}
layerCurentPosition = layer.position();
newLayerTop = layer.clone();
newLayerTop.position({
x: layerCurentPosition.x,
y: layerCurentPosition.y -1960
});
stage.add(newLayerTop)
stage.batchDraw();
}
});
$(document).on('mousemove', function (oEvent) {
if (!bMouseDown) {
return;
}
oDelta = {
'x': oPreviousCoords.x - oEvent.pageX,
'y': oPreviousCoords.y - oEvent.pageY
}
});
But I can't reliably work out the co-ordinates for each direction and then how to reset the camera position.
As you need "infinite" canvas I suggest to not use scrolling and make canvas as large as user viewport. Then you can emulate camera and on every move, you need to draw a new grid on the canvas. You just need to carefully calculate the position of the grid.
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
draggable: true
});
const layer = new Konva.Layer();
stage.add(layer);
const WIDTH = 100;
const HEIGHT = 100;
const grid = [
['red', 'yellow'],
['green', 'blue']
];
function checkShapes() {
const startX = Math.floor((-stage.x() - stage.width()) / WIDTH) * WIDTH;
const endX = Math.floor((-stage.x() + stage.width() * 2) / WIDTH) * WIDTH;
const startY = Math.floor((-stage.y() - stage.height()) / HEIGHT) * HEIGHT;
const endY = Math.floor((-stage.y() + stage.height() * 2) / HEIGHT) * HEIGHT;
for(var x = startX; x < endX; x += WIDTH) {
for(var y = startY; y < endY; y += HEIGHT) {
const indexX = Math.abs(x / WIDTH) % grid.length;
const indexY = Math.abs(y / HEIGHT) % grid[0].length;
layer.add(new Konva.Rect({
x,
y,
width: WIDTH,
height: HEIGHT,
fill: grid[indexX][indexY]
}))
}
}
}
checkShapes();
layer.draw();
stage.on('dragend', () => {
layer.destroyChildren();
checkShapes();
layer.draw();
})
<script src="https://unpkg.com/konva#^2/konva.min.js"></script>
<div id="container"></div>
If you need scrolling you can listen wheel event on stage and move into desired direction.
I'm trying to use Fabric.js with Fabric Brush This issue that I'm running into is that Fabric Brush only puts the brush strokes onto the Top Canvas and not the lower canvas. (The stock brushes in fabric.js save to the bottom canvas) I think I need to convert "this.canvas.contextTop.canvas" to an object and add that object to the the lower canvas. Any ideas?
I've tried running:
this.canvas.add(this.canvas.contextTop)
in
onMouseUp: function (pointer) {this.canvas.add(this.canvas.contextTop)}
But I'm getting the error
Uncaught TypeError: obj._set is not a function
So the contextTop is CanvasHTMLElement context. You cannot add it.
You can add to the fabricJS canvas just fabric.Object derived classes.
Look like is not possible for now.
They draw as pixel effect and then they allow you to export as an image.
Would be nice to extend fabricJS brush interface to create redrawable objects.
As of now with fabricJS and that particular version of fabric brush, the only thing you can do is:
var canvas = new fabric.Canvas(document.getElementById('c'))
canvas.freeDrawingBrush = new fabric.CrayonBrush(canvas, {
width: 70,
opacity: 0.6,
color: "#ff0000"
});
canvas.isDrawingMode = true
canvas.on('mouse:up', function(opt) {
if (canvas.isDrawingMode) {
var c = fabric.util.copyCanvasElement(canvas.upperCanvasEl);
var img = new fabric.Image(c);
canvas.contextTopDirty = true;
canvas.add(img);
canvas.isDrawingMode = false;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.1/fabric.min.js"></script>
<script src="https://tennisonchan.github.io/fabric-brush/bower_components/fabric-brush/dist/fabric-brush.min.js"></script>
<button>Enter free drawing</button>
<canvas id="c" width="500" height="500" ></canvas>
That is just creating an image from the contextTop and add as an object.
I have taken the approach suggested by AndreaBogazzi and modified the Fabric Brush so that it does the transfer from upper to lower canvas (as an image) internal to Fabric Brush. I also used some code I found which crops the image to a smaller bounding box so that is smaller than the full size of the canvas. Each of the brushes in Fabric Brush has an onMouseUp function where the code should be placed. Using the case of the SprayBrush, the original code here was:
onMouseUp: function(pointer) {
},
And it is replaced with this code:
onMouseUp: function(pointer){
function trimbrushandcopytocanvas() {
let ctx = this.canvas.contextTop;
let pixels = ctx.getImageData(0, 0, canvas.upperCanvasEl.width, canvas.upperCanvasEl.height),
l = pixels.data.length,
bound = {
top: null,
left: null,
right: null,
bottom: null
},
x, y;
// Iterate over every pixel to find the highest
// and where it ends on every axis ()
for (let i = 0; i < l; i += 4) {
if (pixels.data[i + 3] !== 0) {
x = (i / 4) % canvas.upperCanvasEl.width;
y = ~~((i / 4) / canvas.upperCanvasEl.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
// Calculate the height and width of the content
var trimHeight = bound.bottom - bound.top,
trimWidth = bound.right - bound.left,
trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight);
// generate a second canvas
var renderer = document.createElement('canvas');
renderer.width = trimWidth;
renderer.height = trimHeight;
// render our ImageData on this canvas
renderer.getContext('2d').putImageData(trimmed, 0, 0);
var img = new fabric.Image(renderer,{
scaleY: 1./fabric.devicePixelRatio,
scaleX: 1./fabric.devicePixelRatio,
left: bound.left/fabric.devicePixelRatio,
top:bound.top/fabric.devicePixelRatio
});
this.canvas.clearContext(ctx);
canvas.add(img);
}
setTimeout(trimbrushandcopytocanvas, this._interval); // added delay because last spray was on delay and may not have finished
},
The setTimeout function was used because Fabric Brush could still be drawing to the upper canvas after the mouseup event occurred, and there were occasions where the brush would continue painting the upper canvas after its context was cleared.