You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
513 lines
19 KiB
513 lines
19 KiB
import { appState } from "../state.js";
|
|
import {
|
|
drawAxes,
|
|
drawPointCloud,
|
|
drawTrajectories,
|
|
drawEgoVehicle,
|
|
drawTrackMarkers,
|
|
drawClusterCentroids,
|
|
drawRegionsOfInterest,
|
|
drawCovarianceEllipse,
|
|
clusterColors, // We need to import clusterColors for the tooltip
|
|
} from "../drawUtils.js";
|
|
import {
|
|
toggleTracks,
|
|
toggleClusterColor,
|
|
togglePredictedPos,
|
|
toggleCovariance,
|
|
} from "../dom.js";
|
|
|
|
function drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY) {
|
|
if (!hoveredItems || hoveredItems.length === 0 || smoothedAvgX === null) return;
|
|
|
|
// 1. Generate text content
|
|
const infoStrings = [];
|
|
for (const item of hoveredItems) {
|
|
let infoText = "";
|
|
let itemColor = item.color || null; // Initialize with existing item color or null
|
|
const data = item.data;
|
|
switch (item.type) {
|
|
case "point":
|
|
const vel = data.velocity !== null ? data.velocity.toFixed(2) : "N/A";
|
|
const snr = data.snr !== null ? data.snr.toFixed(1) : "N/A";
|
|
infoText = `Point ${item.index} | X:${data.x.toFixed(
|
|
2
|
|
)}, Y:${data.y.toFixed(2)} | V:${vel}, SNR:${snr}, Cluster: ${
|
|
data.clusterNumber
|
|
}`;
|
|
break;
|
|
case "cluster":
|
|
const rs =
|
|
data.radialSpeed !== null ? data.radialSpeed.toFixed(2) : "N/A";
|
|
const vx = data.vx !== null ? data.vx.toFixed(2) : "N/A";
|
|
const vy = data.vy !== null ? data.vy.toFixed(2) : "N/A";
|
|
infoText = `Cluster ${data.id} | X:${data.x.toFixed(
|
|
2
|
|
)}, Y:${data.y.toFixed(2)} | rSpeed:${rs}, vX:${vx}, vY:${vy}`;
|
|
break;
|
|
case "track":
|
|
const trackX = data.correctedPosition[0];
|
|
const trackY = data.correctedPosition[1];
|
|
let trackSpeed = "N/A",
|
|
trackVx = "N/A",
|
|
trackVy = "N/A";
|
|
if (
|
|
data.predictedVelocity &&
|
|
data.predictedVelocity[0] !== null &&
|
|
data.predictedVelocity[1] !== null
|
|
) {
|
|
const [vx, vy] = data.predictedVelocity;
|
|
trackVx = vx.toFixed(2);
|
|
trackVy = vy.toFixed(2);
|
|
trackSpeed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1) + " km/h";
|
|
}
|
|
const signText = item.sign ? ` | Sign: ${item.sign}` : "";
|
|
const riskText = item.risk !== null && item.risk !== undefined ? ` | Risk: ${item.risk}` : "";
|
|
const stateText = item.state !== null && item.state !== undefined ? ` | St: ${item.state}` : "";
|
|
infoText = `Track ${item.trackId} | X:${trackX.toFixed(
|
|
2
|
|
)}, Y:${trackY.toFixed(2)} | Speed: ${trackSpeed}${signText}${riskText}${stateText}`;
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
itemColor = isDark
|
|
? p.color(100, 149, 237) // Lighter blue for dark mode
|
|
: p.color(0, 0, 255); // Original blue for light mode
|
|
break;
|
|
case "prediction":
|
|
const p_vx =
|
|
data.predictedVelocity[0] !== null
|
|
? data.predictedVelocity[0].toFixed(2)
|
|
: "N/A";
|
|
const p_vy =
|
|
data.predictedVelocity[1] !== null
|
|
? data.predictedVelocity[1].toFixed(2)
|
|
: "N/A";
|
|
infoText = `Pred. ${
|
|
item.trackId
|
|
} | X:${data.predictedPosition[0].toFixed(
|
|
2
|
|
)}, Y:${data.predictedPosition[1].toFixed(2)} | Vx:${p_vx}, Vy:${p_vy}`;
|
|
itemColor = p.color(255, 0, 0); // Red color for prediction info
|
|
break;
|
|
}
|
|
if (infoText) {
|
|
infoStrings.push({ text: infoText, color: itemColor });
|
|
}
|
|
}
|
|
|
|
// 2. Use filtered screen positions for the tooltip box
|
|
const avgX = smoothedAvgX;
|
|
const avgY = smoothedAvgY;
|
|
|
|
p.push();
|
|
|
|
// --- Start of Tweakable Parameters ---
|
|
const zoomFactor = appState.zoomFactor || 6;
|
|
|
|
// VISUALS: Adjust these numbers to change the tooltip's appearance
|
|
const BASE_FONT_SIZE = 12;
|
|
const BASE_LINE_HEIGHT = 15;
|
|
const BASE_PADDING = 8;
|
|
const BASE_HIGHLIGHT_THICKNESS = 2;
|
|
const BASE_LINE_THICKNESS = 2;
|
|
const BASE_DISTANCE_OFFSET = 65; // <-- How far the tooltip is from the items
|
|
const BASE_VERTICAL_OFFSET = 40; // <-- Upward shift for diagonal effect
|
|
|
|
// COLORS
|
|
const highlightColor = p.color(46, 204, 113); // Green for border and lines
|
|
const bgColor = document.documentElement.classList.contains("dark")
|
|
? p.color(20, 20, 30, 220)
|
|
: p.color(245, 245, 245, 220);
|
|
const defaultTextColor = document.documentElement.classList.contains("dark")
|
|
? p.color(230)
|
|
: p.color(20);
|
|
// --- End of Tweakable Parameters ---
|
|
|
|
// Compensate for zoom factor
|
|
p.textSize(BASE_FONT_SIZE / zoomFactor);
|
|
const lineHeight = BASE_LINE_HEIGHT / zoomFactor;
|
|
const boxPadding = BASE_PADDING / zoomFactor;
|
|
const xOffset = BASE_DISTANCE_OFFSET / zoomFactor;
|
|
const yOffset = BASE_VERTICAL_OFFSET / zoomFactor;
|
|
|
|
let boxWidth = 0;
|
|
infoStrings.forEach((info) => {
|
|
boxWidth = Math.max(boxWidth, p.textWidth(info.text));
|
|
});
|
|
const boxHeight = infoStrings.length * lineHeight + boxPadding * 2;
|
|
boxWidth += boxPadding * 2;
|
|
|
|
// Smart Positioning Logic
|
|
let boxX;
|
|
let anchorOnRight = false;
|
|
|
|
if (mainMouseX > appState.p5_instance.width / 2) {
|
|
boxX = avgX - xOffset - boxWidth;
|
|
anchorOnRight = true;
|
|
} else {
|
|
boxX = avgX + xOffset;
|
|
anchorOnRight = false;
|
|
}
|
|
let boxY = avgY - boxHeight / 2 - yOffset;
|
|
|
|
// --- START: Boundary Constraint Logic ---
|
|
// Calculate the visible bounds in the current coordinate system (which is scaled and translated)
|
|
const visibleW = p.width / zoomFactor;
|
|
const visibleH = p.height / zoomFactor;
|
|
// Use camera center instead of raw mouse position to accurately represent the visible viewport
|
|
const minVisX = smoothedCamX - visibleW / 2;
|
|
const maxVisX = smoothedCamX + visibleW / 2;
|
|
const minVisY = smoothedCamY - visibleH / 2;
|
|
const maxVisY = smoothedCamY + visibleH / 2;
|
|
const edgePad = 10 / zoomFactor;
|
|
|
|
// Constrain X & Y to keep tooltip within the zoom view
|
|
if (boxX + boxWidth > maxVisX - edgePad) boxX = maxVisX - edgePad - boxWidth;
|
|
if (boxX < minVisX + edgePad) boxX = minVisX + edgePad;
|
|
if (boxY + boxHeight > maxVisY - edgePad) boxY = maxVisY - edgePad - boxHeight;
|
|
if (boxY < minVisY + edgePad) boxY = minVisY + edgePad;
|
|
|
|
const connectorAnchorX = anchorOnRight ? boxX + boxWidth : boxX;
|
|
// --- END: Boundary Constraint Logic ---
|
|
|
|
// Draw highlighting circles
|
|
hoveredItems.forEach((item) => {
|
|
p.noFill();
|
|
p.stroke(highlightColor);
|
|
p.strokeWeight(BASE_HIGHLIGHT_THICKNESS / zoomFactor);
|
|
p.ellipse(item.screenX, item.screenY, 15 / zoomFactor, 15 / zoomFactor);
|
|
});
|
|
|
|
// Draw the tooltip box
|
|
p.fill(bgColor);
|
|
p.stroke(highlightColor);
|
|
p.strokeWeight(BASE_LINE_THICKNESS / zoomFactor);
|
|
p.rect(boxX, boxY, boxWidth, boxHeight, 4 / zoomFactor);
|
|
|
|
// Draw the text (with italics for prediction)
|
|
p.noStroke();
|
|
p.textAlign(p.LEFT, p.TOP);
|
|
infoStrings.forEach((info, i) => {
|
|
p.fill(info.color || defaultTextColor);
|
|
if (hoveredItems[i].type === "prediction") {
|
|
p.textStyle(p.ITALIC);
|
|
}
|
|
p.text(info.text, boxX + boxPadding, boxY + boxPadding + i * lineHeight);
|
|
p.textStyle(p.NORMAL); // Reset to normal for the next line
|
|
});
|
|
|
|
// Draw individual connector lines
|
|
hoveredItems.forEach((item, i) => {
|
|
p.stroke(highlightColor);
|
|
p.strokeWeight(BASE_LINE_THICKNESS / zoomFactor);
|
|
const connectorAnchorY =
|
|
boxY + boxPadding + i * lineHeight + lineHeight / 2;
|
|
p.line(connectorAnchorX, connectorAnchorY, item.screenX, item.screenY);
|
|
});
|
|
|
|
p.pop();
|
|
}
|
|
|
|
export const zoomSketch = function (p) {
|
|
let plotScales = { plotScaleX: 1, plotScaleY: 1 };
|
|
let lastUpdate = { mainMouseX: 0, mainMouseY: 0, hoveredItems: [] };
|
|
let canvas = null;
|
|
const containerId = "zoom-canvas-container";
|
|
|
|
// Persistent smoothed coordinates for the tooltip
|
|
let smoothedAvgX = null;
|
|
let smoothedAvgY = null;
|
|
|
|
// Smooth camera coordinates to prevent judder on high-refresh monitors (75Hz+)
|
|
let smoothedCamX = null;
|
|
let smoothedCamY = null;
|
|
|
|
appState.zoomFactor = 4; // Set a default zoom factor in the global state
|
|
appState.zoomLeadFactor = 0.2; // Control how much the circle "leads" the camera (0.0 = smooth, 1.0 = instant)
|
|
|
|
p.setup = function () {
|
|
// Optimization: Increase target frame rate.
|
|
// p5.js often defaults to 60fps. On 75Hz+ screens, this causes frame skipping and judder.
|
|
p.frameRate(144);
|
|
// We enable looping so the lerp smoothing can animate between frames
|
|
p.loop();
|
|
|
|
// --- START: ResizeObserver for ZoomSketch ---
|
|
let resizeDebounce = null;
|
|
const ro = new ResizeObserver(() => {
|
|
if (resizeDebounce) clearTimeout(resizeDebounce);
|
|
resizeDebounce = setTimeout(() => {
|
|
if (canvas) {
|
|
if(typeof p.handleContainerResize === 'function') p.handleContainerResize();
|
|
}
|
|
}, 100);
|
|
});
|
|
const container = document.getElementById(containerId);
|
|
if(container) ro.observe(container);
|
|
// --- END: ResizeObserver for ZoomSketch ---
|
|
};
|
|
|
|
p.updateAndDraw = function (mainMouseX, mainMouseY, hoveredItems, scales) {
|
|
lastUpdate = { mainMouseX, mainMouseY, hoveredItems };
|
|
plotScales = scales;
|
|
if (!canvas) {
|
|
const container = document.getElementById(containerId);
|
|
if (container && container.offsetWidth > 0) {
|
|
canvas = p.createCanvas(container.offsetWidth, container.offsetHeight);
|
|
canvas.parent(containerId);
|
|
//console.log(`zoomSketch: Canvas CREATED with dimensions ${p.width}x${p.height}`); // debug
|
|
} else {
|
|
console.warn(
|
|
"zoomSketch: updateAndDraw called, but container is not ready. Aborting draw."
|
|
); //debug
|
|
return;
|
|
}
|
|
}
|
|
// With loop() enabled, we don't strictly need redraw(),
|
|
// but it helps if updateAndDraw is called less frequently than the frame rate.
|
|
};
|
|
|
|
p.windowResized = function () {}; // Disable native p5 window resize
|
|
|
|
p.handleContainerResize = function () {
|
|
const container = document.getElementById(containerId);
|
|
if (container && canvas) {
|
|
p.resizeCanvas(container.offsetWidth, container.offsetHeight);
|
|
}
|
|
};
|
|
p.draw = function () {
|
|
if (!appState.vizData || !canvas) return;
|
|
p.background(
|
|
document.documentElement.classList.contains("dark")
|
|
? p.color(55, 65, 81)
|
|
: 255
|
|
);
|
|
|
|
const { mainMouseX, mainMouseY, hoveredItems } = lastUpdate;
|
|
|
|
// --- Camera Smoothing (Prevents Judder) ---
|
|
// If the main app updates at 60Hz but this sketch runs at 75Hz, raw coordinates cause stutter.
|
|
if (smoothedCamX === null) {
|
|
smoothedCamX = mainMouseX;
|
|
smoothedCamY = mainMouseY;
|
|
}
|
|
const camSmoothing = 0.5;
|
|
const dt = Math.max(0, p.deltaTime);
|
|
const adjustedCamSmoothing = 1 - Math.pow(1 - camSmoothing, dt / (1000 / 60));
|
|
smoothedCamX = p.lerp(smoothedCamX, mainMouseX, adjustedCamSmoothing);
|
|
smoothedCamY = p.lerp(smoothedCamY, mainMouseY, adjustedCamSmoothing);
|
|
|
|
// --- Tooltip Smoothing (Low Pass Filter) ---
|
|
if (hoveredItems.length > 0) {
|
|
const targetAvgX = hoveredItems.reduce((acc, item) => acc + item.screenX, 0) / hoveredItems.length;
|
|
const targetAvgY = hoveredItems.reduce((acc, item) => acc + item.screenY, 0) / hoveredItems.length;
|
|
|
|
if (smoothedAvgX === null) {
|
|
smoothedAvgX = targetAvgX;
|
|
smoothedAvgY = targetAvgY;
|
|
} else {
|
|
// --- START: Frame-Rate Independent Smoothing ---
|
|
// We use p.deltaTime to adjust the smoothing factor so that the animation
|
|
// speed remains consistent across different monitor refresh rates.
|
|
const baseSmoothing = 0.05; // Target smoothing at 60 FPS
|
|
const dt = Math.max(0, p.deltaTime);
|
|
const adjustedSmoothing = 1 - Math.pow(1 - baseSmoothing, dt / (1000 / 60));
|
|
smoothedAvgX = p.lerp(smoothedAvgX, targetAvgX, adjustedSmoothing);
|
|
smoothedAvgY = p.lerp(smoothedAvgY, targetAvgY, adjustedSmoothing);
|
|
// --- END: Frame-Rate Independent Smoothing ---
|
|
}
|
|
} else {
|
|
smoothedAvgX = null;
|
|
smoothedAvgY = null;
|
|
}
|
|
|
|
p.push(); // Start zoom transformations
|
|
p.translate(
|
|
p.width / 2 - smoothedCamX * appState.zoomFactor,
|
|
p.height / 2 - smoothedCamY * appState.zoomFactor
|
|
);
|
|
p.scale(appState.zoomFactor);
|
|
|
|
// --- Redraw the scene from scratch ---
|
|
if (appState.p5_instance && appState.p5_instance.getStaticBackground) {
|
|
const bg = appState.p5_instance.getStaticBackground();
|
|
// Optimization: Only draw the visible slice of the background
|
|
// Drawing the full 1920x1080 texture every frame is expensive if we only see a tiny part.
|
|
const imgW = bg.width;
|
|
const imgH = bg.height;
|
|
|
|
const visibleW = p.width / appState.zoomFactor;
|
|
const visibleH = p.height / appState.zoomFactor;
|
|
|
|
// Calculate World Coordinates of the top-left of the view
|
|
const sX = smoothedCamX - visibleW / 2;
|
|
const sY = smoothedCamY - visibleH / 2;
|
|
|
|
// Intersect visible view with image bounds
|
|
const dX = Math.max(0, sX);
|
|
const dY = Math.max(0, sY);
|
|
const dW = Math.min(imgW, sX + visibleW) - dX;
|
|
const dH = Math.min(imgH, sY + visibleH) - dY;
|
|
|
|
if (dW > 0 && dH > 0) {
|
|
// Draw only the visible sub-rectangle
|
|
// Since we are transformed to World Space, destination (dx,dy) matches source (dx,dy)
|
|
p.image(bg, dX, dY, dW, dH, dX, dY, dW, dH);
|
|
}
|
|
}
|
|
|
|
p.push(); // Start radar transformations
|
|
p.translate(
|
|
appState.p5_instance.width / 2,
|
|
appState.p5_instance.height * 0.95
|
|
);
|
|
p.scale(1, -1);
|
|
|
|
const frameData = appState.vizData.radarFrames[appState.currentFrame];
|
|
const inverseZoom = 1 / appState.zoomFactor * 2;
|
|
|
|
// --- OPTIMIZATION: Axes and Ego Vehicle are already in the static background image ---
|
|
// drawAxes(p, plotScales);
|
|
// drawEgoVehicle(p, plotScales);
|
|
|
|
if (frameData) {
|
|
drawTrackMarkers(p, plotScales, inverseZoom, false);
|
|
drawRegionsOfInterest(p, frameData, plotScales);
|
|
if (toggleTracks.checked) {
|
|
drawTrajectories(p, plotScales, inverseZoom);
|
|
}
|
|
drawPointCloud(p, frameData.pointCloud, plotScales, 4 * inverseZoom);
|
|
if (toggleClusterColor.checked) {
|
|
drawClusterCentroids(p, frameData.clusters, plotScales, inverseZoom);
|
|
}
|
|
if (togglePredictedPos.checked) {
|
|
for (const track of appState.vizData.tracks) {
|
|
const log = track.historyLog.find(
|
|
(log) => log.frameIdx === appState.currentFrame
|
|
);
|
|
if (
|
|
log &&
|
|
log.predictedPosition &&
|
|
log.predictedPosition[0] !== null
|
|
) {
|
|
const pos = log.predictedPosition;
|
|
const x = pos[0] * plotScales.plotScaleX;
|
|
const y = pos[1] * plotScales.plotScaleY;
|
|
const size = 4 * inverseZoom;
|
|
|
|
p.push();
|
|
p.stroke(255, 0, 0); // Red for predicted
|
|
p.strokeWeight(2 * inverseZoom);
|
|
p.line(x - size, y - size, x + size, y + size);
|
|
p.line(x + size, y - size, x - size, y + size);
|
|
p.pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
p.pop(); // End radar transformations
|
|
|
|
// --- Call the new, self-contained tooltip function with smoothed coords ---
|
|
drawZoomTooltip(p, hoveredItems, mainMouseX, mainMouseY, smoothedAvgX, smoothedAvgY, smoothedCamX, smoothedCamY);
|
|
|
|
// --- START: Draw Purple Debug Circle ---
|
|
// This circle represents the hover radius, drawn in the zoomed coordinate space.
|
|
// The formula must match the one in drawUtils.js.
|
|
const isDark = document.documentElement.classList.contains("dark");
|
|
const hoverRadius = p.constrain(80 / appState.zoomFactor, 5, 25);
|
|
const circleColor = isDark ? p.color(50, 205, 50, 200) : p.color(0, 128, 0, 180);
|
|
|
|
p.push();
|
|
p.noFill();
|
|
p.stroke(circleColor);
|
|
// The stroke weight is divided by the zoom factor to keep it thin.
|
|
p.strokeWeight(1 / appState.zoomFactor);
|
|
p.drawingContext.setLineDash([5 / appState.zoomFactor, 3 / appState.zoomFactor]);
|
|
// The circle is drawn at the mouse position from the main canvas.
|
|
|
|
// Control how much the circle "leads" the camera movement.
|
|
// 0.0 = Locked to center (smooth). 1.0 = Locked to mouse (jumpy/leads).
|
|
const leadFactor = appState.zoomLeadFactor;
|
|
const circleX = p.lerp(smoothedCamX, mainMouseX, leadFactor);
|
|
const circleY = p.lerp(smoothedCamY, mainMouseY, leadFactor);
|
|
p.ellipse(circleX, circleY, hoverRadius * 2, hoverRadius * 2);
|
|
|
|
p.drawingContext.setLineDash([]);
|
|
p.pop();
|
|
// --- END: Draw Purple Debug Circle ---
|
|
|
|
p.pop(); // End zoom transformations
|
|
// --- START: DRAW TITLE OVERLAY ---
|
|
// This code runs *after* the zoom transformations have been popped,
|
|
// so it draws directly onto the canvas as a fixed UI element.
|
|
p.push();
|
|
const titleLabel = document.getElementById("toggle-close-up").parentElement;
|
|
const titleText = titleLabel ? titleLabel.textContent.trim() : "Zoom Mode";
|
|
const textColor = document.documentElement.classList.contains("dark")
|
|
? 220
|
|
: 80;
|
|
p.fill(textColor);
|
|
p.noStroke();
|
|
p.textSize(16);
|
|
p.textAlign(p.LEFT, p.TOP);
|
|
p.textStyle(p.BOLD);
|
|
p.text(titleText, 10, 10);
|
|
p.pop();
|
|
// --- END: DRAW TITLE OVERLAY ---
|
|
|
|
// --- START: Draw Out of Bounds Overlay ---
|
|
if (appState.isMouseOutOfBounds && appState.isCloseUpMode) {
|
|
p.push();
|
|
// Semi-transparent black background
|
|
p.fill(0, 0, 0, 150);
|
|
p.noStroke();
|
|
p.rectMode(p.CENTER);
|
|
p.rect(p.width / 2, p.height / 2, p.width, p.height);
|
|
|
|
// Draw the text
|
|
p.fill(255, 100, 100); // Red warning color
|
|
p.textAlign(p.CENTER, p.CENTER);
|
|
p.textSize(18);
|
|
p.textStyle(p.BOLD);
|
|
|
|
const yOffsetOffset = (appState.zoomCountdown !== null && appState.zoomCountdown > 0) ? 20 : 0;
|
|
p.text("Mouse pointer Out of Bounds", p.width / 2, p.height / 2 - yOffsetOffset);
|
|
|
|
if (appState.zoomCountdown !== null && appState.zoomCountdown > 0) {
|
|
p.fill(255);
|
|
p.textStyle(p.NORMAL);
|
|
p.text(`Closing in ${appState.zoomCountdown}...`, p.width / 2, p.height / 2 + 20);
|
|
}
|
|
p.pop();
|
|
}
|
|
// --- END: Draw Out of Bounds Overlay ---
|
|
// --- START: Draw Countdown Overlay ---
|
|
else if (appState.zoomCountdown !== null && appState.zoomCountdown > 0) {
|
|
p.push();
|
|
// Semi-transparent black background for readability
|
|
p.fill(0, 0, 0, 150);
|
|
p.noStroke();
|
|
p.rectMode(p.CENTER);
|
|
p.rect(p.width / 2, p.height / 2, p.width, p.height);
|
|
|
|
// Draw the text
|
|
p.fill(255);
|
|
p.textAlign(p.CENTER, p.CENTER);
|
|
p.textSize(18);
|
|
p.textStyle(p.NORMAL);
|
|
p.text(
|
|
"Hover over points again to resume the display",
|
|
p.width / 2,
|
|
p.height / 2 - 15
|
|
);
|
|
p.text(`Closing in ${appState.zoomCountdown}...`, p.width / 2, p.height / 2 + 15);
|
|
p.pop();
|
|
}
|
|
// --- END: Draw Countdown Overlay ---
|
|
|
|
// --- Draw Crosshairs ---
|
|
p.stroke(255, 0, 0, 150);
|
|
p.strokeWeight(1.5);
|
|
p.line(p.width / 2 - 15, p.height / 2, p.width / 2 + 15, p.height / 2);
|
|
p.line(p.width / 2, p.height / 2 - 15, p.width / 2, p.height / 2 + 15);
|
|
};
|
|
};
|