Related
I have made a marker that draws when the mouse click is held down. I now want to change the size of this marker relative to the speed of the cursor. My code is as follows:
class App {
constructor(event, x) {
this.container = this.createContainer();
this.canvas = this.createCanvas();
this.ctx = this.canvas.getContext("2d");
this.addEventListeners();
this.isDrawing = this.letDrawing();
}
createContainer = () => {
const container = document.createElement("div");
container.className = "app-container";
document.body.appendChild(container);
return container;
};
createCanvas = () => {
const canvas = document.createElement("canvas");
canvas.className = "app-canvas";
this.container.appendChild(canvas);
canvas.width = this.container.getBoundingClientRect().width;
canvas.height = this.container.getBoundingClientRect().height;
return canvas;
};
addEventListeners = () => {
this.canvas.addEventListener("mousemove", this.onMouseMove);
this.canvas.addEventListener("mousedown", this.onMouseDown);
document.addEventListener("mouseup", this.onMouseUp);
};
letDrawing = () => {
this.isDrawing = false;
};
onMouseDown = () => {
this.isDrawing = true;
};
onMouseMove = (event) => {
if (this.isDrawing === true) {
this.drawBall(event.x, event.y);
}
};
onMouseUp = () => {
this.isDrawing = false;
};
drawBall = (x, y) => {
this.ctx.beginPath();
this.ctx.arc(x, y, 0, Math.PI * 2);
this.ctx.fillStyle = "#6f0ecf";
this.ctx.fill();
this.ctx.closePath();
};
}
export { App };
I was thinking of using previous position of X and Y, and doing something like below... Can I use deltaX in this scenario?
Any help on how to do this would be much appreciated!!
onMouseMove = (event) => {
if (this.isDrawing === true) {
this.drawBall(event.x, event.y);
this.previousX = this.currentX;
this.currentX = event.x;
distance = this.currentX - this.previousX;
}
};
onMouseMoveSpeed = () => {
const size = speed * 10 + 10;
};
onMouseUp = () => {
this.isDrawing = false;
};
drawBall = (x, y, size) => {
this.ctx.beginPath();
this.ctx.arc(x, y, size, 0, Math.PI * 2);
this.ctx.fillStyle = "#6f0ecf";
this.ctx.fill();
this.ctx.closePath();
};
yes, you'll need to save the last position and the last time the event occured and them compute the speed as the distance from the last position divided by the time difference.
let last = {
x: 0,
y: 0,
time: 0
}
window.onmousemove = function(evt) {
let
x = evt.clientX,
y = evt.clientY,
time = new Date;
let dist = Math.sqrt((last.x - x) ** 2 + (last.y - y) ** 2)
let speed = dist / (time - last.time) * 1000
last = {x, y, time}
console.log('speed=', speed.toFixed(0))
}
I'm trying to make a simple interactive game where there are circles of different colours moving in the canvas and when the user clicks on the blue circles, it logs the number of clicks on the screen. When clicking circles with any other colour, the animation stops.
I'm very new to javascript but this is what I have for now. I've made a function with random coloured circles and a function with blue circles moving but I'm totally stuck on how to stop the animation when clicking on the function with a random coloured circles and logging the amount of clicks on the blue circles. If someone could help me move forward with it in any way (doesn't have to be the full thing), that would be awesome, thanks.
JS
var canvas;
var ctx;
var w = 1000;
var h = 600;
var colours = ["red", "blue"];
var allCircles = [];
for(var i=0; i<1; i++){
setTimeout(function(){console.log(i)},1000);
}
document.querySelector("#myCanvas").onclick = click;
createData(2);
createDataTwo(20);
setUpCanvas();
animationLoop();
function animationLoop(){
clear();
for(var i = 0; i<allCircles.length; i++){
circle(allCircles[i]);
forward(allCircles[i], 5)
turn(allCircles[i], randn(30));
collisionTestArray(allCircles[i],allCircles)
bounce(allCircles[i]);
}
requestAnimationFrame(animationLoop);
}
function collisionTestArray(o, a){
for(var i=0; i<a.length; i++){
if(o !=a[i]){
collision(o,a[i]);
}
}
}
function collision(o1,o2){
if(o1 && o2){
var differencex = Math.abs(o1.x-o2.x);
var differencey = Math.abs(o1.y-o2.y);
var hdif = Math.sqrt(differencex*differencex+differencey*differencey);
if(hdif<o1.r+o2.r){
if(differencex < differencey){
turn(o1, 180-2*o1.angle);
turn(o2, 180-2*o2.angle);
}else{
turn(o1, 360-2*o1.angle);
turn(o2, 360-2*o2.angle);
}
turn(o1, 180);
turn(o2, 180);
console.log("collision");
};
}
}
function click(event){
clear()
}
function bounce (o){
if(o.x > w || o.x < 0){
turn(o, 180-2*o.angle);
};
if(o.y > h || o.y < 0){
turn(o, 360-2*o.angle);
}
}
function clear(){
ctx.clearRect(0,0,w,h);
}
function stop (){
o1.changex = 0;
o1.changey = 0;
o2.changex = 0;
o2.changey = 0;
}
function circle (o){
var x = o.x;
var y = o.y;
var a = o.angle;
var d = o.d;
ctx.beginPath();
ctx.arc(o.x,o.y,o.r,0,2*Math.PI);
ctx.fillStyle = "hsla("+o.c+",100%,50%, "+o.a+")";
ctx.fill();
o.x = x;
o.y = y;
o.angle = a;
o.d = d;
}
function createData(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":216,
"r": 50
}
)
}
}
function createDataTwo(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":rand(90),
"r": 50
}
)
}
}
function turn(o,angle){
if(angle != undefined){
o.changle=angle;
};
o.angle+=o.changle;
}
function forward(o,d){
var changeX;
var changeY;
var oneDegree = Math.PI/180;
if(d != undefined){
o.d = d;
};
changeX= o.d*Math.cos(o.angle*oneDegree);
changeY = o.d*Math.sin(o.angle*oneDegree);
o.x+=changeX;
o.y+=changeY;
}
function randn(r){
var result = Math.random()*r - r/2
return result
}
function randi(r) {
var result = Math.floor(Math.random()*r);
return result
}
function rand(r){
return Math.random()*r
}
function setUpCanvas(){
canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
canvas.style.border = "5px solid orange"
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, w, h);
}
console.log("assi4")
HTML
<html>
<head>
<link rel="stylesheet" type="text/css" href="../modules.css">
</head>
<body>
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id = "myCanvas"></canvas>
</div>
<script src="assi5.js"></script>
</body>
</html>
CSS
#container {
margin: auto;
width: 75%;
text-align: center;
}
You can use cancelAnimationFrame to stop the animation when a non-blue circle is clicked
You need to pass it a reference to the frame ID returned from requestAnimationFrame for it to work.
In order to tell if a circle was clicked, you need to check the coordinates of each circle against the coordinates of the click.
I have an example below if you had your blue circles in an array "blue", and other circles in array "other", the ID returned by requestAnimationFrame as "frame".
The check function returns the number of blue circles hit (the points scored) and if any other circles were hit, it stops the animation.
getCoords returns the coordinates of the click on the canvas from the click event.
canvas.addEventListener('click', event=>{
points += check(getCoords(event), blue, other, frame);
document.getElementById('points').textContent = points;
})
function check({x, y}, blue, other, frame) {
other.filter(circle=>circle.isWithin(x, y))
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle=>circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
I have an example that works below where I changed the circles to the result of a function rather than an inline object, and moved the functions you use on them into their own class. You don't have to do this, but I find it a lot easier to understand.
function main() {
const canvas = document.getElementById('canvas');
const context = canvas.getContext("2d");
const clear = () => context.clearRect(0, 0, canvas.width, canvas.height);
const blue = new Array(2).fill().map(() => new Circle(context, 216));
const other = new Array(10).fill().map(() => new Circle(context));
let circles = [...blue, ...other];
let frame = 0;
let points = 0;
// Move the circle a bit and check if it needs to bounce
function update(circle) {
circle.forward(1)
circle.turn(30, true)
circle.collisionTestArray(circles)
circle.bounce();
}
// Main game loop, clear canvas, update circle positions, draw circles
function loop() {
clear();
circles.filter(circle => circle.free).forEach(update);
circles.forEach(circle => circle.draw());
frame = requestAnimationFrame(loop);
}
loop();
canvas.addEventListener('click', event => {
points += check(getCoords(event), blue, other, frame, circles);
document.getElementById('points').textContent = points;
})
}
function check({ x, y }, blue, other, frame) {
other.filter(circle => circle.isWithin(x, y))
// .map(circle=>circle.toggle())
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle => circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
main();
function Circle(context, c) {
const randn = r => rand(r) - r / 2;
const randi = r => Math.floor(randi(r));
const rand = r => Math.random() * r;
// These are for easily stopping and starting a circle;
this.free = true;
this.stop = () => this.free = false;
this.release = () => this.free = true;
this.toggle = () => this.free = !this.free;
const {
width,
height
} = context.canvas;
// These are the same properties you were using in your code
this.x = rand(width);
this.changex = rand(10);
this.y = rand(height);
this.changey = rand(10);
this.w = randn(width);
this.h = randn(height);
this.d = 1;
this.a = 1;
this.angle = 0;
this.changle = 15;
this.c = c || rand(90); // This is the only difference between blue and other circles
this.r = 50;
// These next functions you had in your code, I just moved them into the circle definition
this.draw = () => {
const { x, y, r, c } = this;
context.beginPath();
context.arc(x, y, r, 0, 2 * Math.PI);
context.fillStyle = "hsla(" + c + ",100%,50%, 1)";
context.fill();
}
this.bounce = () => {
const { x, y, angle } = this;
if (x > width || x < 0) {
this.turn(180 - 2 * angle);
}
if (y > height || y < 0) {
this.turn(360 - 2 * angle);
}
}
this.turn = (angle, random = false) => {
this.changle = random ? randn(angle) : angle;
this.angle += this.changle;
}
this.forward = d => {
this.d = d;
this.x += this.d * Math.cos(this.angle * Math.PI / 180);
this.y += this.d * Math.sin(this.angle * Math.PI / 180);
}
this.collisionTestArray = a => a
.filter(circle => circle != this)
.forEach(circle => this.collision(circle));
this.collision = circle => {
var differencex = Math.abs(this.x - circle.x);
var differencey = Math.abs(this.y - circle.y);
var hdif = Math.sqrt(differencex ** 2 + differencey ** 2);
if (hdif < this.r + circle.r) {
if (differencex < differencey) {
this.turn(180 - 2 * this.angle);
circle.turn(180 - 2 * circle.angle);
} else {
this.turn(360 - 2 * this.angle);
circle.turn(360 - 2 * circle.angle);
}
this.turn(180);
circle.turn(180);
}
}
// These 2 functions I added to check if the circle was clicked
this.distanceFrom = (x, y) => Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
this.isWithin = (x, y) => this.r > this.distanceFrom(x, y);
}
#canvas {
border: 5px solid orange;
}
#container {
margin: auto;
width: 75%;
text-align: center;
}
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id="canvas" width="1000" height="600"></canvas>
<p>
Points: <span id="points">0</span>
</p>
</div>
using OOP is better in this situation and will save you a lot of time
I have written the OOP version of your game, I wrote it in harry so you may find some bugs but it is good as a starting point
const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
let h = canvas.height = 600
let w = canvas.width = 800
const numberOfCircles = 20
let circles = []
// running gameover
let gameStatus = "running"
let score = 0
canvas.addEventListener("click", (e) => {
if(gameStatus === "gameOver") {
document.location.reload()
return;
}
const mouse = {x: e.offsetX, y: e.offsetY}
for(let circle of circles) {
if(distance(mouse, circle) <= circle.radius) {
if(circle.color == "blue") {
gameStatus = "running"
score += 1
} else {
gameStatus = "gameOver"
}
}
}
})
class Circle {
constructor(x, y, color, angle) {
this.x = x
this.y = y
this.color = color
this.radius = 15
this.angle = angle
this.speed = 3
}
draw(ctx) {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
move(circles) {
this.check(circles)
this.x += Math.cos(this.angle) * this.speed
this.y += Math.sin(this.angle) * this.speed
}
check(circles) {
if(this.x + this.radius > w || this.x - this.radius < 0) this.angle += Math.PI
if(this.y + this.radius > h || this.y - this.radius < 0) this.angle += Math.PI
for(let circle of circles) {
if(circle === this) continue
if(distance(this, circle) <= this.radius + circle.radius) {
// invert angles or any other effect
// there are much better soultion for resolving colliusions
circle.angle += Math.PI / 2
this.angle += Math.PI / 2
}
}
}
}
}
setUp()
gameLoop()
function gameLoop() {
ctx.clearRect(0,0,w,h)
if(gameStatus === "gameOver") {
ctx.font = "30px Comic"
ctx.fillText("Game Over", w/2 - 150, h/2 - 100)
ctx.fillText("you have scored : " + score, w/2 - 150, h/2)
return;
}
ctx.font = "30px Comic"
ctx.fillText("score : " + score, 20, 30)
for (let i = 0; i < circles.length; i++) {
const cirlce = circles[i]
cirlce.draw(ctx)
cirlce.move(circles)
}
requestAnimationFrame(gameLoop)
}
function random(to, from = 0) {
return Math.floor(Math.random() * (to - from) + from)
}
function setUp() {
gameStatus = "running"
score = 0
circles = []
for (var i = 0; i < numberOfCircles; i++) {
const randomAngle = random(360) * Math.PI / 180
circles.push(new Circle(random(w, 20), random(h, 20), randomColor(), randomAngle))
}
}
function randomColor() {
const factor = random(10)
if(factor < 3) return "blue"
return `rgb(${random(255)}, ${random(255)}, ${random(100)})`
}
function distance(obj1, obj2) {
const xDiff = obj1.x - obj2.x
const yDiff = obj1.y - obj2.y
return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2))
}
I don't know if this makes me lazy or what I just to use this kind method but it doesn't work well in react.
Here is the link of the code
HOVER
So basically my error here is the part of ctx = canvas.getContext('2d') I try to copy all the codes and do this something in my react
useEffect(() => {
const rippleSettings = {
maxSize: 100,
animationSpeed: 5,
strokeColor: [148, 217, 255],
};
const canvasSettings = {
blur: 8,
ratio: 1,
};
function Coords(x, y) {
this.x = x || null;
this.y = y || null;
}
const Ripple = function Ripple(x, y, circleSize, ctx) {
this.position = new Coords(x, y);
this.circleSize = circleSize;
this.maxSize = rippleSettings.maxSize;
this.opacity = 1;
this.ctx = ctx;
this.strokeColor = `rgba(${Math.floor(rippleSettings.strokeColor[0])},
${Math.floor(rippleSettings.strokeColor[1])},
${Math.floor(rippleSettings.strokeColor[2])},
${this.opacity})`;
this.animationSpeed = rippleSettings.animationSpeed;
this.opacityStep = (this.animationSpeed / (this.maxSize - circleSize)) / 2;
};
Ripple.prototype = {
update: function update() {
this.circleSize = this.circleSize + this.animationSpeed;
this.opacity = this.opacity - this.opacityStep;
this.strokeColor = `rgba(${Math.floor(rippleSettings.strokeColor[0])},
${Math.floor(rippleSettings.strokeColor[1])},
${Math.floor(rippleSettings.strokeColor[2])},
${this.opacity})`;
},
draw: function draw() {
this.ctx.beginPath();
this.ctx.strokeStyle = this.strokeColor;
this.ctx.arc(this.position.x, this.position.y, this.circleSize, 0,
2 * Math.PI);
this.ctx.stroke();
},
setStatus: function setStatus(status) {
this.status = status;
},
};
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
console.log(ctx)
const ripples = [];
const rippleStartStatus = 'start';
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
canvas.style.filter = `blur(${canvasSettings.blur}px)`;
canvas.width = width * canvasSettings.ratio;
canvas.height = height * canvasSettings.ratio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
let animationFrame;
// Function which is executed on mouse hover on canvas
const canvasMouseOver = (e) => {
const x = e.clientX * canvasSettings.ratio;
const y = e.clientY * canvasSettings.ratio;
ripples.unshift(new Ripple(x, y, 2, ctx));
};
const animation = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const length = ripples.length;
for (let i = length - 1; i >= 0; i -= 1) {
const r = ripples[i];
r.update();
r.draw();
if (r.opacity <= 0) {
ripples[i] = null;
delete ripples[i];
ripples.pop();
}
}
animationFrame = window.requestAnimationFrame(animation);
};
animation();
canvas.addEventListener('mousemove', canvasMouseOver);
})
I removed the part of GUI based on the link because I don't need the settings. So what I want here is the getContext('2d') I don't understand why it is wrong. Can anyone figure out it for me? Should I be asking this question? I feel so unlucky right now hahha badly need help.
I am trying to put a word search game on my web page.
I see here is a beautiful example: Embed a Word Search in Your Site
I want to make similar gui, so that when the user selects the word letters from the grid by dragging mouse, it will draw a red elastic rubber band around the letters selected.
How do they do it on their web page? I tried to "inspect" the html after the band is drawn, however did not see any element added.
Can somebody give me any clue?
Here is a JS example
class App {
constructor(div) {
this.div = div;
this.canvas = document.getElementById("canvas");
this.ctx = canvas.getContext('2d');
this.dirty = true;
this.prev = +new Date();
this.resize();
window.addEventListener('resize', () => this.resize(), false);
this.canvas.addEventListener("mousedown", (event) => {
this.touchdown(...App.getmousePos(event));
event.preventDefault();
});
this.canvas.addEventListener("mousemove", (event) => {
this.touchmove(...App.getmousePos(event));
event.preventDefault();
});
this.canvas.addEventListener("mouseup", (event) => {
this.touchup(...App.getmousePos(event));
event.preventDefault();
});
this.canvas.addEventListener("touchstart", (event) => {
this.touchdown(...App.getmousePos(event));
event.preventDefault();
});
this.canvas.addEventListener("touchmove", (event) => {
this.touchmove(App.getmousePos(event));
event.preventDefault();
});
this.canvas.addEventListener("touchend", (event) => {
this.touchup(App.getmousePos(event));
event.preventDefault();
});
}
resize() {
this.canvas.width = this.div.clientWidth;
this.canvas.height = this.div.clientWidth;
this.draw();
}
loop() {
let now = +new Date();
let dt = now - this.prev;
this.prev = now;
this.update()
if (this.dirty)
this.draw();
window.requestAnimationFrame(() => this.loop());
}
update(dt) {}
draw() {
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.ctx.rect(0, 0, 100, 100);
this.ctx.fill();
}
touchdown(x, y) { console.log("down", x, y); }
touchmove(x, y) {}
touchup(x, y) {}
static getmousePos(event) {
if (event.changedTouches) {
return [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
}
else {
var rect = event.target.getBoundingClientRect();
return [event.clientX- rect.left, event.clientY - rect.top];
}
}
}
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
add(v_x, y) {
if (y != undefined) {
return new Vector(this.x + v_x, this.y + y);
}
else {
return new Vector(this.x + v_x.x, this.y + v_x.y);
}
}
sub(v_x, y) {
if (y != undefined) {
return new Vector(this.x - v_x, this.y - y);
}
else {
return new Vector(this.x - v_x.x, this.y - v_x.y);
}
}
mul(s) {
return new Vector(this.x * s, this.y * s);
}
get slength() {
return this.x * this.x + this.y * this.y;
}
get length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
get angle() {
return Math.atan2(this.y, this.x);
}
}
class Grid {
constructor(width, height, data) {
this.width = width;
this.height = height;
this.data = data || new Array(width*height).fill("_");
}
get length() {
return this.width*this.height;
}
get(x_pos, y) {
if (y != undefined)
return this.data[x_pos+y*this.width];
else
return this.data[x_pos];
}
set(value, x_pos, y) {
if (y != undefined)
this.data[x_pos+y*this.width] = value;
else
this.data[x_pos] = value;
}
clone() {
return new Grid(this.width, this.height, this.data.slice());
}
finalize() {
this.data = this.data.map(v => v == "_" ? String.fromCharCode(Math.floor(Math.random() * 26) + 97) : v);
}
print() {
for (let i = 0; i < this.height; i++)
console.log(this.data.slice(i*this.width, (i+1)*this.width).join(","));
}
}
class Puzzle {
constructor(width, height, words, directions) {
this.grid = new Grid(width, height);
this.words = words;
this.placeWords(words, directions);
}
placeWords(words, directions) {
const range = (N) => Array.from({length: N}, (v, k) => k);
const shuffle = (array) => {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
words = words.slice();
let positions = range(this.grid.length);
let stack = [ {
grid: this.grid,
word: words.shift(),
directions: shuffle(directions.slice()),
positions: shuffle(positions.slice()) } ];
while (true) {
let current = stack[stack.length-1];
if (!current)
throw Error("impossible");
let dir = current.directions.pop();
if (!dir) {
current.positions.pop();
current.directions = shuffle(directions.slice());
dir = current.directions.pop();
}
if (current.positions.length <= 0) {
words.unshift(current.word);
stack.pop();
}
else {
let pos = current.positions[current.positions.length-1];
let grid = this.placeWord(current.grid, current.word, pos, dir);
if (grid) {
if (words.length > 0) {
stack.push({grid: grid,
word: words.shift(),
directions: shuffle(directions.slice()),
positions: shuffle(positions.slice())});
}
else {
grid.finalize();
this.grid = grid;
break;
}
}
}
}
}
placeWord(grid, word, position, direction) {
let copy = grid.clone();
position = new Vector(position % grid.width, Math.floor(position / grid.width));
let letters = [...word];
while (0 <= position.x && position.x < grid.width && 0 <= position.y && position.y < grid.height) {
if (letters.length <= 0)
break;
let letter = letters.shift();
if (copy.get(position.x, position.y) == "_" || copy.get(position.x, position.y) == letter) {
copy.set(letter, position.x, position.y);
position = position.add(direction);
}
else {
return null;
}
}
if (letters.length > 0)
return null;
else
return copy;
}
wordAt(position, direction, length) {
let word = new Array(length);
for (let i = 0; i < length; i++) {
word[i] = this.grid.get(position.x, position.y);
position = position.add(direction);
}
return word.join("");
}
print() {
this.grid.print();
}
}
class Selection {
constructor(position, fill=false) {
this.position = position;
this._fill = fill;
this.direction = new Vector(1, 0);
this.length = 0;
this.flength = 0;
}
clone() {
return new Selection(this.position).to(this.position.add(this.direction.mul(this.length)));
}
to(position) {
let direction = position.sub(this.position);
if (Math.abs(direction.y) == 0) {
this.direction = new Vector(direction.x >= 0 ? 1 : -1, 0);
this.length = direction.x * this.direction.x;
}
else if (Math.abs(direction.x) == 0) {
this.direction = new Vector(0, direction.y >= 0 ? 1 : -1);
this.length = direction.y * this.direction.y;
}
else {
this.direction = new Vector(direction.x >= 0 ? 1 : -1, direction.y >= 0 ? 1 : -1);
this.length = direction.x * this.direction.x;
}
this.flength = direction.length;
return this;
}
fill(fill=true) {
this._fill = fill;
return this;
}
draw(ctx, wsize, hsize) {
let pos = this.position.mul(wsize);
let length = wsize * (this.flength+1);
let angle = this.direction.angle;
//console.log(this.x, this.y, x, y, length, angle, this.dx, this.dy);
ctx.save();
ctx.strokeStyle = "black";
ctx.fillStyle = "yellow";
ctx.translate(pos.x+hsize*0.5, pos.y+hsize*0.5);
ctx.rotate(angle);
this.drawHighlight(ctx, -hsize*0.5, -hsize*0.5, length, hsize, hsize*0.5, this._fill, true);
ctx.restore();
}
drawHighlight(ctx, x, y, width, height, radius=20, fill=true, stroke=false) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
}
class PuzzleApp extends App {
constructor(div, puzzle) {
super(div);
this.puzzle = puzzle;
this.renderList(document.getElementById("list"));
this.selections = new Array();
this.loop();
}
renderList(parent) {
this.puzzle.words.forEach(word => {
let li = document.createElement("li");
let text = document.createTextNode(word);
li.appendChild(text);
parent.appendChild(li);
});
}
gridSize() {
let wsize = Math.floor(this.canvas.width/this.puzzle.grid.width);
let hsize = Math.floor(this.canvas.width/this.puzzle.grid.height);
return [wsize, hsize];
}
clientToGrid(x, y) {
let [wsize, hsize] = this.gridSize();
x = Math.floor(x / wsize);
y = Math.floor(y / hsize);
return [x, y];
}
draw() {
if (!this.puzzle)
return;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
let [wsize, hsize] = this.gridSize();
this.selections.forEach(s => s.draw(this.ctx, wsize, hsize));
if (this.selection)
this.selection.draw(this.ctx, wsize, hsize);
let x = 0;
let y = 0;
this.ctx.fillStyle = "black";
this.ctx.font = (wsize * 0.5) + 'px sans-serif';
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
for (let j = 0; j < this.puzzle.grid.height; j++) {
for (let i = 0; i < this.puzzle.grid.width; i++) {
let letter = this.puzzle.grid.get(i, j);
this.ctx.fillText(letter, x+wsize * 0.5, y+wsize * 0.5);
x += wsize;
}
x = 0;
y += wsize;
}
}
touchdown(x, y) {
[x, y] = this.clientToGrid(x, y);
this.selection = new Selection(new Vector(x, y));
this.dirty = true;
}
touchmove(x, y) {
if (!this.selection)
return;
[x, y] = this.clientToGrid(x, y);
this.selection.to(new Vector(x, y));
}
touchup(x, y) {
if (!this.selection)
return;
let word = this.puzzle.wordAt(this.selection.position, this.selection.direction, this.selection.length+1);
console.log(word);
if (word) {
let list = document.getElementById("list");
let elements = list.getElementsByTagName("li");
Array.prototype.some.call(elements, li => {
if (li.innerText == word) {
li.classList.add("found");
this.selections.push(this.selection.clone().fill());
return true;
}
return false;
});
}
this.selection = null;
}
}
let app = null;
document.addEventListener("DOMContentLoaded", function(event) {
const wordLists = [
["pear", "apple", "banana", "peach", "kiwi", "prune", "persimon"]
]
const pick = (array) => array[Math.floor(Math.random() * (array.length))];
let params = (new URL(document.location)).searchParams;
let directions = [new Vector(1,0), new Vector(0,1)];
if (params.get("diagonal")) {
directions.push(new Vector(1,1));
}
if (params.get("backwards")) {
directions.push(new Vector(-1,0));
directions.push(new Vector(0,-1));
if (params.get("diagonal")) {
directions.push(new Vector(-1,-1));
}
}
let puzzle = new Puzzle(8, 8, pick(wordLists), directions);
puzzle.print();
app = new PuzzleApp(document.getElementById("canvasContainer"), puzzle);
});
html, body {
width: 100%;
height: 100%;
margin: 0px;
}
ul {
display: inline-block;
font-size: 7vmin;
padding-left: 14vmin;
}
li.found {
text-decoration: line-through;
}
div#canvasContainer {
width: 100%;
display: inline-block;
}
#media (min-aspect-ratio: 1/1) {
ul {
font-size: 4vmin;
padding-left: 7vmin;
}
div#canvasContainer {
width: 50%;
}
}
<div id="canvasContainer"><canvas id="canvas" width="320" height="320"></canvas></div>
<ul id="list"></ul>
I'll let you figure out how to change the thickness and color of the rubber band.
The red band is an svg. It doesn't get added when you click, but rather it gets redrawn.
I have inspected the referenced website and it seems like they draw rubber band thing in SVG. If you inspect this SVG tag while drawing:
you will notice it shows some updates.
You can use same SVG methodology. Or Another way is to use an absolute positioned div and update top, left css values based on mousedown, width based on mousemove event.
I have seen "what" they have done.
They have created an svg element. This svg sits exactly above and covers the entire table that contains the grid with letters. When the user clicks any letter and starts drag, they add a rectangle to the svg. Depending on how the cursor will move they do the angle transform to the rectangle. If the word is selected correctly, the rectangle survives; else, it is removed.
I need to find out now, "how" part of it.
Thanks to all for a very quick response.
I've tried for the last few days without too much success to rotate, scale and translate shapes on the canvas.
I've read everything I could find on internet about similar issues but still I cannot seem to be able to adapt it to my own problem.
If everything is drawn on the same scale, I can still drag and drop. If I rotate the shapes, then the mouseOver is messed up since the world coordinates don't correspond anymore with the shape coordinates.
If I scale, then it's impossible to select any shape.
I look at my code and do not understand what I'm doing wrong.
I read some really nice and detailed stackoverflow solutions to similar problems.
For example, user #blindman67 made a suggestion of using a setTransform helper and a getMouseLocal helper for getting the coordinates of the mouse relative to the transformed shape.
inverse transform matrix
I spent some time with that and could not fix my issue.
Here is an example of what I tried. Any suggestion is appreciated.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = '';
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = 'SELECT';
let shapes = {};
const selectButton = document.getElementById('select');
const rectangleButton = document.getElementById('rectangle');
canvas.addEventListener('mousedown', canvasMouseDown);
canvas.addEventListener('mouseup', canvasMouseUp);
canvas.addEventListener('mousemove', canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
startX = mouseX;
startY = mouseY;
mouseIsDown = true;
selectedShape = '';
if (selectedTool === 'SELECT') {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver(mouseX, mouseY)) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
endX = mouseX;
endY = mouseY;
mouseIsDown = false;
const tooSmallShape = Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === 'RECTANGLE') {
const newShape = new Shape(selectedTool.toLowerCase(), startX, startY, endX, endY);
shapes[newShape.id] = newShape;
selectedShape = '';
setActiveTool('SELECT');
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === 'SELECT' && selectedShape !== '') {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === 'RECTANGLE') {
drawShapeGhost(mouseX, mouseY);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
};
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || 'rgba(149, 160, 178, 0.8)';
this.stroke = stroke || 'rgba(0, 0, 0, 0.8)';
this.rotation = 0;
this.isSelected = false;
this.scale = 1;
}
drawShape(ctx) {
switch (this.type) {
case 'rectangle':
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
ctx.scale(this.scale, this.scale);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
ctx.rotate(degreesToRadians(this.rotation));
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = 'rgba(254, 0, 0, 1)';
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height)
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver(mouseX, mouseY) {
if (this.type === 'rectangle') {
return (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height);
}
}
}
const menu = document.getElementById('menu');
const rotation = document.getElementById('rotation');
const scaleSlider = document.getElementById('scale');
menu.addEventListener('click', onMenuClick);
rotation.addEventListener('input', onRotationChange);
scaleSlider.addEventListener('input', onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool && tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== '') {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale = scale;
}
draw();
}
function setTransform(ctx, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
ctx.setTransform(xDx * scaleX, xDy * scaleX, -xDy * scaleY, xDx * scaleY, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
const cross = xDx * scaleX * xDx * scaleY - xDy * scaleX * (-xDy) * scaleY;
const ixDx = (xDx * scaleY) / cross;
const ixDy = (-xDy * scaleX) / cross;
const iyDx = (xDy * scaleY) / cross;
const iyDy = (xDx * scaleX) / cross;
mouseX -= x;
mouseY -= y;
const localMouseX = mouseX * ixDx + mouseY * iyDx;
const localMouseY = mouseX * ixDy + mouseY * iyDy;
return {
x: localMouseX,
y: localMouseY,
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180
};
function radiansToDegrees(radians) {
return radians * 180 / Math.PI
};
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>
If I have time tomorrow I will try to implement the following to your code but I can provide you with a working example of how to get mouse collision precision on a rotated rectangle. I had the same struggle with this and finally found a good explanation and code that I was able to get to work. Check out this website
Now for my own implementation I did not use the method on that website to get my vertices. As you'll see in my code I have a function called updateCorners() in my Square class. I also have objects called this.tl.x and this.tl.y (for each corner).
The formulas are what I use to get vertices of a translated and rotated rectangle and the corner objects are what are used to determine collision. From there I used the distance() function (Pythagorean theorem), the triangleArea() function, and then the clickHit() function which I renamed to collision() and changed some things.
Example in the snippet below
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let shapes = [];
let mouse = {
x: null,
y: null
}
canvas.addEventListener('mousemove', e => {
mouse.x = e.x - canvas.getBoundingClientRect().x;
mouse.y = e.y - canvas.getBoundingClientRect().y;
})
class Square {
constructor(x, y, w, h, c) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.c = c;
this.a = 0;
this.r = this.a * (Math.PI/180);
this.cx = this.x + this.w/2;
this.cy = this.y + this.h/2;
//used to track corners
this.tl = {x: 0, y: 0};
this.tr = {x: 0, y: 0};
this.br = {x: 0, y: 0};
this.bl = {x: 0, y: 0};
}
draw() {
ctx.save();
ctx.translate(this.x, this.y)
ctx.rotate(this.r);
ctx.fillStyle = this.c;
ctx.fillRect(-this.w/2,-this.h/2,this.w,this.h);
ctx.restore();
}
updateCorners() {
this.a += 0.1
this.r = this.a * (Math.PI/180);
let cos = Math.cos(this.r);
let sin = Math.sin(this.r)
//updates Top Left Corner
this.tl.x = (this.x-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2);
this.tl.y = (this.x-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Top Right Corner
this.tr.x = ((this.x+this.w)-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2)
this.tr.y = ((this.x+this.w)-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Right Corner
this.br.x = ((this.x+this.w)-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.br.y = ((this.x+this.w)-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Left Corner
this.bl.x = (this.x-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.bl.y = (this.x-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
}
}
let square1 = shapes.push(new Square(250, 70, 25, 25, 'red'));
let square2 = shapes.push(new Square(175,210, 100, 50, 'blue'));
let square3 = shapes.push(new Square(50,100, 30, 50, 'purple'));
let square4 = shapes.push(new Square(140,120, 120, 20, 'pink'));
//https://joshuawoehlke.com/detecting-clicks-rotated-rectangles/
//pythagorean theorm using built in javascript hypot
function distance(p1, p2) {
return Math.hypot(p1.x-p2.x, p1.y-p2.y);
}
//Heron's formula used to determine area of triangle
//in the collision() function we will break the rectangle into triangles
function triangleArea(d1, d2, d3) {
var s = (d1 + d2 + d3) / 2;
return Math.sqrt(s * (s - d1) * (s - d2) * (s - d3));
}
function collision(mouse, rect) {
//area of rectangle
var rectArea = Math.round(rect.w * rect.h);
// Create an array of the areas of the four triangles
var triArea = [
// mouse posit checked against tl-tr
triangleArea(
distance(mouse, rect.tl),
distance(rect.tl, rect.tr),
distance(rect.tr, mouse)
),
// mouse posit checked against tr-br
triangleArea(
distance(mouse, rect.tr),
distance(rect.tr, rect.br),
distance(rect.br, mouse)
),
// mouse posit checked against tr-bl
triangleArea(
distance(mouse, rect.br),
distance(rect.br, rect.bl),
distance(rect.bl, mouse)
),
// mouse posit checked against bl-tl
triangleArea(
distance(mouse, rect.bl),
distance(rect.bl, rect.tl),
distance(rect.tl, mouse)
)
];
let triArea2 = Math.round(triArea.reduce(function(a,b) { return a + b; }, 0));
if (triArea2 > rectArea) {
return false;
}
return true;
}
function animate() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+',y: '+mouse.y, 50, 50);
for (let i=0; i< shapes.length; i++) {
shapes[i].draw();
shapes[i].updateCorners();
if (collision(mouse, shapes[i])) {
shapes[i].c = 'red';
} else {
shapes[i].c = 'green'
}
}
requestAnimationFrame(animate)
}
animate();
<canvas id="canvas"></canvas>
I'm sure there's many other ways to do this but this is what I was able to understand and get to work. I haven't really messed with scale so I can't help much there.
UPDATE:
Here is a snippet using the method you wanted. Now you can rotate, scale, and translate and still click inside the shape. Be aware I changed your mouse to a global mouse object vice making it a variable in every mouse... function.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = "";
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = "SELECT";
let localMouse = { x: null, y: null };
let mouse = { x: null, y: null };
let shapes = {};
const selectButton = document.getElementById("select");
const rectangleButton = document.getElementById("rectangle");
canvas.addEventListener("mousedown", canvasMouseDown);
canvas.addEventListener("mouseup", canvasMouseUp);
canvas.addEventListener("mousemove", canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
startX = mouse.x;
startY = mouse.y;
mouseIsDown = true;
selectedShape = "";
if (selectedTool === "SELECT") {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver()) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
endX = mouse.x;
endY = mouse.y;
mouseIsDown = false;
const tooSmallShape =
Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === "RECTANGLE") {
const newShape = new Shape(
selectedTool.toLowerCase(),
startX,
startY,
endX,
endY
);
shapes[newShape.id] = newShape;
selectedShape = "";
setActiveTool("SELECT");
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === "SELECT" && selectedShape !== "") {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === "RECTANGLE") {
drawShapeGhost(mouse.x, mouse.y);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
}
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || "rgba(149, 160, 178, 0.8)";
this.stroke = stroke || "rgba(0, 0, 0, 0.8)";
this.rotation = 0;
this.isSelected = false;
this.scale = { x: 1, y: 1 };
}
drawShape(ctx) {
switch (this.type) {
case "rectangle":
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
setTransform(
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = "rgba(254, 0, 0, 1)";
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver() {
localMouse = getMouseLocal(
mouse.x,
mouse.y,
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.type === "rectangle") {
if (
localMouse.x > 0 - this.width / 2 &&
localMouse.x < 0 + this.width / 2 &&
localMouse.y < 0 + this.height / 2 &&
localMouse.y > 0 - this.height / 2
) {
return true;
}
}
}
}
const menu = document.getElementById("menu");
const rotation = document.getElementById("rotation");
const scaleSlider = document.getElementById("scale");
menu.addEventListener("click", onMenuClick);
rotation.addEventListener("input", onRotationChange);
scaleSlider.addEventListener("input", onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool && tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== "") {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale.x = scale;
shape.scale.y = scale;
}
draw();
}
function setTransform(x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
ctx.setTransform(xdx * sx, xdy * sx, -xdy * sy, xdx * sy, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
var cross = xdx * sx * xdx * sy - xdy * sx * -xdy * sy;
var ixdx = (xdx * sy) / cross; // create inverted x axis
var ixdy = (-xdy * sx) / cross;
var iydx = (xdy * sy) / cross; // create inverted y axis
var iydy = (xdx * sx) / cross;
mouseX -= x;
mouseY -= y;
var localMouseX = mouseX * ixdx + mouseY * iydx;
var localMouseY = mouseX * ixdy + mouseY * iydy;
return { x: localMouseX, y: localMouseY };
}
function radiansToDegrees(radians) {
return (radians * 180) / Math.PI;
}
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<button id="triangle" data-tool="TRIANGLE">triangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>