import simplify from "simplify-js"; import * as Vector2 from "./vector2"; import { toDegrees } from "./shared"; import colors from "./colors"; const snappingThreshold = 1 / 5; export function getBrushPositionForTool( brushPosition, tool, toolSettings, gridSize, shapes ) { let position = brushPosition; if (tool === "shape") { const snapped = Vector2.roundTo(position, gridSize); const minGrid = Vector2.min(gridSize); const distance = Vector2.length(Vector2.subtract(snapped, position)); if (distance < minGrid * snappingThreshold) { position = snapped; } } if (tool === "fog" && toolSettings.type === "add") { if (toolSettings.useGridSnapping) { position = Vector2.roundTo(position, gridSize); } if (toolSettings.useEdgeSnapping) { const minGrid = Vector2.min(gridSize); let closestDistance = Number.MAX_VALUE; let closestPosition = position; // Find the closest point on all fog shapes for (let shape of shapes) { if (shape.type === "fog") { const points = shape.data.points; const isInShape = Vector2.pointInPolygon(position, points); // Find the closest point to each line of the shape for (let i = 0; i < points.length; i++) { const a = points[i]; // Wrap around points to the start to account for closed shape const b = points[(i + 1) % points.length]; const { distance: distanceToLine, point: pointOnLine, } = Vector2.distanceToLine(position, a, b); const isCloseToShape = distanceToLine < minGrid * snappingThreshold; if ( (isInShape || isCloseToShape) && distanceToLine < closestDistance ) { closestPosition = pointOnLine; closestDistance = distanceToLine; } } } } position = closestPosition; } } return position; } export function getDefaultShapeData(type, brushPosition) { if (type === "circle") { return { x: brushPosition.x, y: brushPosition.y, radius: 0 }; } else if (type === "rectangle") { return { x: brushPosition.x, y: brushPosition.y, width: 0, height: 0, }; } else if (type === "triangle") { return { points: [ { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, { x: brushPosition.x, y: brushPosition.y }, ], }; } } export function getGridScale(gridSize) { if (gridSize.x < gridSize.y) { return { x: gridSize.y / gridSize.x, y: 1 }; } else if (gridSize.y < gridSize.x) { return { x: 1, y: gridSize.x / gridSize.y }; } else { return { x: 1, y: 1 }; } } export function getUpdatedShapeData(type, data, brushPosition, gridSize) { const gridScale = getGridScale(gridSize); if (type === "circle") { const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y, }); const scaled = Vector2.multiply(dif, gridScale); const distance = Vector2.length(scaled); return { ...data, radius: distance, }; } else if (type === "rectangle") { const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y }); return { ...data, width: dif.x, height: dif.y, }; } else if (type === "triangle") { const points = data.points; const dif = Vector2.subtract(brushPosition, points[0]); // Scale the distance by the grid scale then unscale before adding const scaled = Vector2.multiply(dif, gridScale); const length = Vector2.length(scaled); const direction = Vector2.normalize(scaled); // Get the angle for a triangle who's width is the same as it's length const angle = Math.atan(length / 2 / length); const sideLength = length / Math.cos(angle); const leftDir = Vector2.rotateDirection(direction, toDegrees(angle)); const rightDir = Vector2.rotateDirection(direction, -toDegrees(angle)); const leftDirUnscaled = Vector2.divide(leftDir, gridScale); const rightDirUnscaled = Vector2.divide(rightDir, gridScale); return { points: [ points[0], Vector2.add(Vector2.multiply(leftDirUnscaled, sideLength), points[0]), Vector2.add(Vector2.multiply(rightDirUnscaled, sideLength), points[0]), ], }; } } const defaultStrokeSize = 1 / 10; export function getStrokeSize(multiplier, gridSize, canvasWidth, canvasHeight) { const gridPixelSize = Vector2.multiply(gridSize, { x: canvasWidth, y: canvasHeight, }); return Vector2.min(gridPixelSize) * defaultStrokeSize * multiplier; } export function shapeHasFill(shape) { return ( shape.type === "fog" || shape.type === "shape" || (shape.type === "path" && shape.pathType === "fill") ); } export function pointsToQuadraticBezier(points) { const quadraticPoints = []; // Draw a smooth curve between the points where each control point // is the current point in the array and the next point is the center of // the current point and the next point for (let i = 1; i < points.length - 2; i++) { const start = points[i - 1]; const controlPoint = points[i]; const next = points[i + 1]; const end = Vector2.divide(Vector2.add(controlPoint, next), 2); quadraticPoints.push({ start, controlPoint, end }); } // Curve through the last two points quadraticPoints.push({ start: points[points.length - 2], controlPoint: points[points.length - 1], end: points[points.length - 1], }); return quadraticPoints; } export function pointsToPathSmooth(points, close, canvasWidth, canvasHeight) { const path = new Path2D(); if (points.length < 2) { return path; } path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight); const quadraticPoints = pointsToQuadraticBezier(points); for (let quadPoint of quadraticPoints) { const pointScaled = Vector2.multiply(quadPoint.end, { x: canvasWidth, y: canvasHeight, }); const controlScaled = Vector2.multiply(quadPoint.controlPoint, { x: canvasWidth, y: canvasHeight, }); path.quadraticCurveTo( controlScaled.x, controlScaled.y, pointScaled.x, pointScaled.y ); } if (close) { path.closePath(); } return path; } export function pointsToPathSharp(points, close, canvasWidth, canvasHeight) { const path = new Path2D(); path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight); for (let point of points.slice(1)) { path.lineTo(point.x * canvasWidth, point.y * canvasHeight); } if (close) { path.closePath(); } return path; } export function circleToPath(x, y, radius, canvasWidth, canvasHeight) { const path = new Path2D(); const minSide = canvasWidth < canvasHeight ? canvasWidth : canvasHeight; path.arc( x * canvasWidth, y * canvasHeight, radius * minSide, 0, 2 * Math.PI, true ); return path; } export function rectangleToPath( x, y, width, height, canvasWidth, canvasHeight ) { const path = new Path2D(); path.rect( x * canvasWidth, y * canvasHeight, width * canvasWidth, height * canvasHeight ); return path; } export function shapeToPath(shape, canvasWidth, canvasHeight) { const data = shape.data; if (shape.type === "path") { return pointsToPathSmooth( data.points, shape.pathType === "fill", canvasWidth, canvasHeight ); } else if (shape.type === "shape") { if (shape.shapeType === "circle") { return circleToPath( data.x, data.y, data.radius, canvasWidth, canvasHeight ); } else if (shape.shapeType === "rectangle") { return rectangleToPath( data.x, data.y, data.width, data.height, canvasWidth, canvasHeight ); } else if (shape.shapeType === "triangle") { return pointsToPathSharp(data.points, true, canvasWidth, canvasHeight); } } else if (shape.type === "fog") { return pointsToPathSharp( shape.data.points, true, canvasWidth, canvasHeight ); } } export function isShapeHovered( shape, context, hoverPosition, canvasWidth, canvasHeight ) { const path = shapeToPath(shape, canvasWidth, canvasHeight); if (shapeHasFill(shape)) { return context.isPointInPath( path, hoverPosition.x * canvasWidth, hoverPosition.y * canvasHeight ); } else { return context.isPointInStroke( path, hoverPosition.x * canvasWidth, hoverPosition.y * canvasHeight ); } } export function drawShape(shape, context, gridSize, canvasWidth, canvasHeight) { const path = shapeToPath(shape, canvasWidth, canvasHeight); const color = colors[shape.color] || shape.color; const fill = shapeHasFill(shape); context.globalAlpha = shape.blend ? 0.5 : 1.0; context.fillStyle = color; context.strokeStyle = color; if (shape.strokeWidth > 0) { context.lineCap = "round"; context.lineWidth = getStrokeSize( shape.strokeWidth, gridSize, canvasWidth, canvasHeight ); context.stroke(path); } if (fill) { context.fill(path); } } const defaultSimplifySize = 1 / 100; export function simplifyPoints(points, gridSize, scale) { return simplify( points, (Vector2.min(gridSize) * defaultSimplifySize) / scale ); } export function getRelativePointerPosition(event, container) { if (container) { const containerRect = container.getBoundingClientRect(); const x = (event.clientX - containerRect.x) / containerRect.width; const y = (event.clientY - containerRect.y) / containerRect.height; return { x, y }; } }