shapefinder / index.html
MarkArtist21's picture
undefined - Initial Deployment
8665e07 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Bezier-Bands Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/control-p5/1.6.0/controlP5.min.js"></script>
<style>
.band-fill-0 { fill: rgba(255, 99, 132, 0.3); }
.band-fill-1 { fill: rgba(54, 162, 235, 0.3); }
.band-fill-2 { fill: rgba(255, 206, 86, 0.3); }
.band-fill-3 { fill: rgba(75, 192, 192, 0.3); }
.band-fill-4 { fill: rgba(153, 102, 255, 0.3); }
.band-stroke-0 { stroke: rgba(255, 99, 132, 1); }
.band-stroke-1 { stroke: rgba(54, 162, 235, 1); }
.band-stroke-2 { stroke: rgba(255, 206, 86, 1); }
.band-stroke-3 { stroke: rgba(75, 192, 192, 1); }
.band-stroke-4 { stroke: rgba(153, 102, 255, 1); }
#canvas-container {
position: relative;
width: 100%;
height: 100%;
}
#processing-canvas {
width: 100%;
height: 100%;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
</style>
</head>
<body class="bg-gray-100 h-screen flex flex-col">
<div class="bg-indigo-700 text-white p-4 shadow-md">
<h1 class="text-2xl font-bold">Interactive Bezier-Bands Tool</h1>
<p class="text-sm opacity-80">Create smooth curves with offset bands and transformations</p>
</div>
<div class="flex flex-1 overflow-hidden">
<!-- Control Panel -->
<div class="w-80 bg-white p-4 shadow-md overflow-y-auto">
<div class="space-y-6">
<div>
<h2 class="text-lg font-semibold mb-2 border-b pb-2">Curve Parameters</h2>
<div class="space-y-4">
<div>
<label for="numCurves" class="block text-sm font-medium text-gray-700 tooltip">
Number of Curves (N)
<span class="tooltiptext">Total number of curves including the base curve (min 2, max 50)</span>
</label>
<input type="range" id="numCurves" min="2" max="50" value="10" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>2</span>
<span id="numCurvesValue">10</span>
<span>50</span>
</div>
</div>
<div>
<label for="offsetSpacing" class="block text-sm font-medium text-gray-700 tooltip">
Offset Spacing (D)
<span class="tooltiptext">Distance between each offset curve in pixels</span>
</label>
<input type="range" id="offsetSpacing" min="1" max="50" value="20" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>1</span>
<span id="offsetSpacingValue">20</span>
<span>50</span>
</div>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-semibold mb-2 border-b pb-2">Base Transform</h2>
<div class="space-y-4">
<div>
<label for="baseOffsetX" class="block text-sm font-medium text-gray-700 tooltip">
Base X Offset
<span class="tooltiptext">Initial X offset applied to the first band</span>
</label>
<input type="range" id="baseOffsetX" min="-100" max="100" value="0" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>-100</span>
<span id="baseOffsetXValue">0</span>
<span>100</span>
</div>
</div>
<div>
<label for="baseOffsetY" class="block text-sm font-medium text-gray-700 tooltip">
Base Y Offset
<span class="tooltiptext">Initial Y offset applied to the first band</span>
</label>
<input type="range" id="baseOffsetY" min="-100" max="100" value="0" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>-100</span>
<span id="baseOffsetYValue">0</span>
<span>100</span>
</div>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-semibold mb-2 border-b pb-2">Incremental Transform</h2>
<div class="space-y-4">
<div>
<label for="deltaOffsetX" class="block text-sm font-medium text-gray-700 tooltip">
X Offset Increment
<span class="tooltiptext">Additional X offset added to each subsequent band</span>
</label>
<input type="range" id="deltaOffsetX" min="-20" max="20" value="0" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>-20</span>
<span id="deltaOffsetXValue">0</span>
<span>20</span>
</div>
</div>
<div>
<label for="deltaOffsetY" class="block text-sm font-medium text-gray-700 tooltip">
Y Offset Increment
<span class="tooltiptext">Additional Y offset added to each subsequent band</span>
</label>
<input type="range" id="deltaOffsetY" min="-20" max="20" value="0" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>-20</span>
<span id="deltaOffsetYValue">0</span>
<span>20</span>
</div>
</div>
<div>
<label for="deltaRotationDeg" class="block text-sm font-medium text-gray-700 tooltip">
Rotation Increment (deg)
<span class="tooltiptext">Additional rotation applied to each subsequent band in degrees</span>
</label>
<input type="range" id="deltaRotationDeg" min="-15" max="15" value="0" class="w-full mt-1">
<div class="flex justify-between text-xs text-gray-500">
<span>-15</span>
<span id="deltaRotationDegValue">0</span>
<span>15</span>
</div>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-semibold mb-2 border-b pb-2">Anchor Point</h2>
<div class="flex items-center space-x-2">
<input type="radio" id="anchorCentroid" name="anchorType" value="centroid" checked class="h-4 w-4 text-indigo-600">
<label for="anchorCentroid" class="text-sm font-medium text-gray-700">Centroid</label>
<input type="radio" id="anchorBounds" name="anchorType" value="bounds" class="h-4 w-4 text-indigo-600">
<label for="anchorBounds" class="text-sm font-medium text-gray-700">Bounds Center</label>
</div>
</div>
<div class="space-y-2">
<h2 class="text-lg font-semibold mb-2 border-b pb-2">Actions</h2>
<button id="exportSVG" class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700 transition">
Export SVG
</button>
<button id="savePNG" class="w-full bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700 transition">
Save PNG
</button>
<button id="clearPoints" class="w-full bg-red-600 text-white py-2 px-4 rounded hover:bg-red-700 transition">
Clear Points
</button>
<button id="resetView" class="w-full bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700 transition">
Reset View
</button>
</div>
<div class="text-xs text-gray-500">
<p><strong>Instructions:</strong></p>
<ul class="list-disc pl-5 space-y-1 mt-1">
<li>Left-click to add points</li>
<li>Drag points to move them</li>
<li>Right-click a point to delete it</li>
<li>Press 'C' to clear all points</li>
</ul>
</div>
</div>
</div>
<!-- Canvas Area -->
<div class="flex-1 bg-gray-200 relative">
<div id="canvas-container">
<div id="processing-canvas"></div>
</div>
<div id="status-message" class="absolute top-4 left-4 bg-white bg-opacity-90 p-2 rounded shadow-md hidden">
<p id="status-text" class="text-sm"></p>
</div>
</div>
</div>
<script>
// Global variables
let points = [];
let selectedPoint = null;
let baseCurve = [];
let offsetCurves = [];
let bands = [];
let viewOffset = { x: 0, y: 0 };
let viewScale = 1;
let isDragging = false;
let lastMouseX, lastMouseY;
// DOM elements
const numCurvesSlider = document.getElementById('numCurves');
const offsetSpacingSlider = document.getElementById('offsetSpacing');
const baseOffsetXSlider = document.getElementById('baseOffsetX');
const baseOffsetYSlider = document.getElementById('baseOffsetY');
const deltaOffsetXSlider = document.getElementById('deltaOffsetX');
const deltaOffsetYSlider = document.getElementById('deltaOffsetY');
const deltaRotationDegSlider = document.getElementById('deltaRotationDeg');
const exportSVGBtn = document.getElementById('exportSVG');
const savePNGBtn = document.getElementById('savePNG');
const clearPointsBtn = document.getElementById('clearPoints');
const resetViewBtn = document.getElementById('resetView');
const statusMessage = document.getElementById('status-message');
const statusText = document.getElementById('status-text');
// Display values
const numCurvesValue = document.getElementById('numCurvesValue');
const offsetSpacingValue = document.getElementById('offsetSpacingValue');
const baseOffsetXValue = document.getElementById('baseOffsetXValue');
const baseOffsetYValue = document.getElementById('baseOffsetYValue');
const deltaOffsetXValue = document.getElementById('deltaOffsetXValue');
const deltaOffsetYValue = document.getElementById('deltaOffsetYValue');
const deltaRotationDegValue = document.getElementById('deltaRotationDegValue');
// Update display values when sliders change
numCurvesSlider.addEventListener('input', () => {
numCurvesValue.textContent = numCurvesSlider.value;
updateCurves();
});
offsetSpacingSlider.addEventListener('input', () => {
offsetSpacingValue.textContent = offsetSpacingSlider.value;
updateCurves();
});
baseOffsetXSlider.addEventListener('input', () => {
baseOffsetXValue.textContent = baseOffsetXSlider.value;
updateCurves();
});
baseOffsetYSlider.addEventListener('input', () => {
baseOffsetYValue.textContent = baseOffsetYSlider.value;
updateCurves();
});
deltaOffsetXSlider.addEventListener('input', () => {
deltaOffsetXValue.textContent = deltaOffsetXSlider.value;
updateCurves();
});
deltaOffsetYSlider.addEventListener('input', () => {
deltaOffsetYValue.textContent = deltaOffsetYSlider.value;
updateCurves();
});
deltaRotationDegSlider.addEventListener('input', () => {
deltaRotationDegValue.textContent = deltaRotationDegSlider.value;
updateCurves();
});
// Button event listeners
clearPointsBtn.addEventListener('click', () => {
points = [];
updateCurves();
showStatus("All points cleared");
});
resetViewBtn.addEventListener('click', () => {
viewOffset = { x: 0, y: 0 };
viewScale = 1;
updateCurves();
showStatus("View reset");
});
exportSVGBtn.addEventListener('click', () => {
showStatus("SVG export would be implemented in Processing");
});
savePNGBtn.addEventListener('click', () => {
showStatus("PNG save would be implemented in Processing");
});
// Show status message
function showStatus(message, duration = 3000) {
statusText.textContent = message;
statusMessage.classList.remove('hidden');
if (duration > 0) {
setTimeout(() => {
statusMessage.classList.add('hidden');
}, duration);
}
}
// Initialize p5.js sketch
const sketch = (p) => {
p.setup = () => {
const canvas = p.createCanvas(p.windowWidth - 320, p.windowHeight - 60);
canvas.parent('processing-canvas');
p.background(240);
p.noFill();
p.stroke(0);
p.strokeWeight(1);
// Handle mouse events
canvas.mousePressed((e) => {
if (e.button === 0) { // Left click
handleLeftClick(p.mouseX, p.mouseY);
}
});
canvas.mouseReleased(() => {
selectedPoint = null;
});
canvas.mouseMoved(() => {
if (!isDragging) {
checkPointHover(p.mouseX, p.mouseY);
}
});
canvas.mouseDragged((e) => {
if (e.button === 0 && selectedPoint) {
// Move the selected point
selectedPoint.x = (p.mouseX - viewOffset.x) / viewScale;
selectedPoint.y = (p.mouseY - viewOffset.y) / viewScale;
updateCurves();
} else if (e.button === 0) {
// Pan the view
if (!isDragging) {
isDragging = true;
lastMouseX = p.mouseX;
lastMouseY = p.mouseY;
} else {
const dx = p.mouseX - lastMouseX;
const dy = p.mouseY - lastMouseY;
viewOffset.x += dx;
viewOffset.y += dy;
lastMouseX = p.mouseX;
lastMouseY = p.mouseY;
p.redraw();
}
}
});
canvas.mouseWheel((e) => {
// Zoom the view
const zoomFactor = e.delta > 0 ? 0.9 : 1.1;
const mouseX = p.mouseX;
const mouseY = p.mouseY;
// Calculate the mouse position in world coordinates
const worldX = (mouseX - viewOffset.x) / viewScale;
const worldY = (mouseY - viewOffset.y) / viewScale;
// Apply zoom
viewScale *= zoomFactor;
// Adjust the offset so the zoom is centered on the mouse
viewOffset.x = mouseX - worldX * viewScale;
viewOffset.y = mouseY - worldY * viewScale;
p.redraw();
return false;
});
// Handle right click (context menu)
canvas.elt.addEventListener('contextmenu', (e) => {
e.preventDefault();
handleRightClick(p.mouseX, p.mouseY);
return false;
});
// Handle keyboard events
document.addEventListener('keydown', (e) => {
if (e.key === 'c' || e.key === 'C') {
points = [];
updateCurves();
showStatus("All points cleared");
} else if ((e.key === 'Backspace' || e.key === 'Delete') && selectedPoint) {
points = points.filter(p => p !== selectedPoint);
selectedPoint = null;
updateCurves();
showStatus("Point deleted");
}
});
};
p.draw = () => {
p.background(240);
p.push();
p.translate(viewOffset.x, viewOffset.y);
p.scale(viewScale);
// Draw the bands
drawBands(p);
// Draw the base curve
if (baseCurve.length > 0) {
p.stroke(0, 0, 255);
p.strokeWeight(2 / viewScale);
p.noFill();
p.beginShape();
for (const pt of baseCurve) {
p.vertex(pt.x, pt.y);
}
p.endShape();
}
// Draw the control points
for (const pt of points) {
if (pt === selectedPoint) {
p.fill(255, 0, 0);
p.stroke(255, 0, 0);
} else if (pt.hovered) {
p.fill(255, 165, 0);
p.stroke(255, 165, 0);
} else {
p.fill(0);
p.stroke(0);
}
p.strokeWeight(1 / viewScale);
p.ellipse(pt.x, pt.y, 8 / viewScale, 8 / viewScale);
}
p.pop();
// Reset dragging state when mouse is released
if (!p.mouseIsPressed) {
isDragging = false;
}
};
p.windowResized = () => {
p.resizeCanvas(p.windowWidth - 320, p.windowHeight - 60);
};
};
new p5(sketch);
// Handle left click to add points
function handleLeftClick(mouseX, mouseY) {
// Convert to world coordinates
const worldX = (mouseX - viewOffset.x) / viewScale;
const worldY = (mouseY - viewOffset.y) / viewScale;
// Check if we're clicking near an existing point
let clickedOnPoint = false;
for (const pt of points) {
const d = Math.sqrt((pt.x - worldX) ** 2 + (pt.y - worldY) ** 2);
if (d < 10 / viewScale) {
selectedPoint = pt;
clickedOnPoint = true;
break;
}
}
if (!clickedOnPoint) {
// Add a new point
points.push({ x: worldX, y: worldY, hovered: false });
updateCurves();
showStatus("Point added");
}
}
// Handle right click to delete points
function handleRightClick(mouseX, mouseY) {
// Convert to world coordinates
const worldX = (mouseX - viewOffset.x) / viewScale;
const worldY = (mouseY - viewOffset.y) / viewScale;
// Find the closest point
let closestPoint = null;
let minDist = Infinity;
for (const pt of points) {
const d = Math.sqrt((pt.x - worldX) ** 2 + (pt.y - worldY) ** 2);
if (d < 10 / viewScale && d < minDist) {
closestPoint = pt;
minDist = d;
}
}
if (closestPoint) {
points = points.filter(p => p !== closestPoint);
if (selectedPoint === closestPoint) {
selectedPoint = null;
}
updateCurves();
showStatus("Point deleted");
}
}
// Check if mouse is hovering over a point
function checkPointHover(mouseX, mouseY) {
// Convert to world coordinates
const worldX = (mouseX - viewOffset.x) / viewScale;
const worldY = (mouseY - viewOffset.y) / viewScale;
let anyHovered = false;
for (const pt of points) {
const d = Math.sqrt((pt.x - worldX) ** 2 + (pt.y - worldY) ** 2);
pt.hovered = d < 10 / viewScale;
if (pt.hovered) anyHovered = true;
}
document.body.style.cursor = anyHovered ? 'pointer' : 'default';
}
// Update the curves and bands based on current points and settings
function updateCurves() {
if (points.length < 2) {
baseCurve = [];
offsetCurves = [];
bands = [];
return;
}
// Compute the base curve as a Catmull-Rom spline
computeBaseCurve();
// Compute offset curves
computeOffsetCurves();
// Compute bands
computeBands();
}
// Compute the base curve using Catmull-Rom spline
function computeBaseCurve() {
baseCurve = [];
if (points.length < 2) return;
// Number of samples per segment
const samplesPerSegment = 50;
for (let i = 0; i < points.length - 1; i++) {
const p0 = i === 0 ? points[0] : points[i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = i === points.length - 2 ? points[points.length - 1] : points[i + 2];
for (let j = 0; j < samplesPerSegment; j++) {
const t = j / samplesPerSegment;
const point = catmullRom(t, p0, p1, p2, p3);
baseCurve.push(point);
}
}
// Add the last point
baseCurve.push({ x: points[points.length - 1].x, y: points[points.length - 1].y });
}
// Catmull-Rom interpolation
function catmullRom(t, p0, p1, p2, p3) {
// tension = 0.5 for Catmull-Rom
const tension = 0.5;
const t2 = t * t;
const t3 = t2 * t;
// The matrix coefficients
const m0 = (-tension * t3) + (2 * tension * t2) + (-tension * t);
const m1 = ((2 - tension) * t3) + (tension - 3) * t2 + 1;
const m2 = (tension - 2) * t3 + (3 - 2 * tension) * t2 + tension * t;
const m3 = tension * t3 - tension * t2;
return {
x: m0 * p0.x + m1 * p1.x + m2 * p2.x + m3 * p3.x,
y: m0 * p0.y + m1 * p1.y + m2 * p2.y + m3 * p3.y
};
}
// Compute offset curves
function computeOffsetCurves() {
offsetCurves = [];
if (baseCurve.length < 2) return;
const numCurves = parseInt(numCurvesSlider.value);
const offsetSpacing = parseInt(offsetSpacingSlider.value);
// First curve is the base curve
offsetCurves.push([...baseCurve]);
// Compute normals for the base curve
const normals = computeNormals(baseCurve);
// Create offset curves
for (let k = 1; k < numCurves; k++) {
const offsetDistance = k * offsetSpacing;
const offsetCurve = [];
for (let i = 0; i < baseCurve.length; i++) {
const pt = baseCurve[i];
const normal = normals[i];
offsetCurve.push({
x: pt.x + normal.x * offsetDistance,
y: pt.y + normal.y * offsetDistance
});
}
offsetCurves.push(offsetCurve);
}
}
// Compute normals for a polyline
function computeNormals(polyline) {
const normals = [];
for (let i = 0; i < polyline.length; i++) {
let normal;
if (i === 0) {
// First point - use next segment
const dx = polyline[i + 1].x - polyline[i].x;
const dy = polyline[i + 1].y - polyline[i].y;
normal = normalize({ x: -dy, y: dx });
} else if (i === polyline.length - 1) {
// Last point - use previous segment
const dx = polyline[i].x - polyline[i - 1].x;
const dy = polyline[i].y - polyline[i - 1].y;
normal = normalize({ x: -dy, y: dx });
} else {
// Middle point - average adjacent segments
const dx1 = polyline[i].x - polyline[i - 1].x;
const dy1 = polyline[i].y - polyline[i - 1].y;
const n1 = normalize({ x: -dy1, y: dx1 });
const dx2 = polyline[i + 1].x - polyline[i].x;
const dy2 = polyline[i + 1].y - polyline[i].y;
const n2 = normalize({ x: -dy2, y: dx2 });
normal = normalize({
x: (n1.x + n2.x) / 2,
y: (n1.y + n2.y) / 2
});
}
normals.push(normal);
}
return normals;
}
// Normalize a vector
function normalize(v) {
const length = Math.sqrt(v.x * v.x + v.y * v.y);
if (length === 0) return { x: 0, y: 0 };
return { x: v.x / length, y: v.y / length };
}
// Compute bands between offset curves
function computeBands() {
bands = [];
if (offsetCurves.length < 2) return;
const baseOffsetX = parseInt(baseOffsetXSlider.value);
const baseOffsetY = parseInt(baseOffsetYSlider.value);
const deltaOffsetX = parseInt(deltaOffsetXSlider.value);
const deltaOffsetY = parseInt(deltaOffsetYSlider.value);
const deltaRotationDeg = parseInt(deltaRotationDegSlider.value);
const anchorType = document.querySelector('input[name="anchorType"]:checked').value;
for (let i = 0; i < offsetCurves.length - 1; i++) {
const band = {
polygon: [],
centroid: { x: 0, y: 0 }
};
// Create polygon by combining curve i (forward) and curve i+1 (reverse)
for (const pt of offsetCurves[i]) {
band.polygon.push({ x: pt.x, y: pt.y });
}
for (let j = offsetCurves[i + 1].length - 1; j >= 0; j--) {
band.polygon.push({
x: offsetCurves[i + 1][j].x,
y: offsetCurves[i + 1][j].y
});
}
// Close the polygon
band.polygon.push({
x: offsetCurves[i][0].x,
y: offsetCurves[i][0].y
});
// Compute centroid or bounds center
band.centroid = computeCentroid(band.polygon);
// Apply transforms
const offsetX = baseOffsetX + i * deltaOffsetX;
const offsetY = baseOffsetY + i * deltaOffsetY;
const rotation = i * deltaRotationDeg * Math.PI / 180;
for (const pt of band.polygon) {
// Translate to origin, rotate, then translate back
const translatedX = pt.x - band.centroid.x;
const translatedY = pt.y - band.centroid.y;
const rotatedX = translatedX * Math.cos(rotation) - translatedY * Math.sin(rotation);
const rotatedY = translatedX * Math.sin(rotation) + translatedY * Math.cos(rotation);
pt.x = rotatedX + band.centroid.x + offsetX;
pt.y = rotatedY + band.centroid.y + offsetY;
}
// Update centroid after transform
band.centroid.x += offsetX;
band.centroid.y += offsetY;
bands.push(band);
}
}
// Compute centroid of a polygon
function computeCentroid(polygon) {
let area = 0;
let cx = 0;
let cy = 0;
for (let i = 0; i < polygon.length - 1; i++) {
const x0 = polygon[i].x;
const y0 = polygon[i].y;
const x1 = polygon[i + 1].x;
const y1 = polygon[i + 1].y;
const a = x0 * y1 - x1 * y0;
area += a;
cx += (x0 + x1) * a;
cy += (y0 + y1) * a;
}
area /= 2;
const centroid = {
x: cx / (6 * area),
y: cy / (6 * area)
};
return centroid;
}
// Draw all bands
function drawBands(p) {
if (bands.length === 0) return;
// Draw from inner to outer
for (let i = bands.length - 1; i >= 0; i--) {
const band = bands[i];
// Alternate colors
const colorIndex = i % 5;
p.fill(0, 0, 255, 30);
p.stroke(0, 0, 255);
p.strokeWeight(1 / viewScale);
p.beginShape();
for (const pt of band.polygon) {
p.vertex(pt.x, pt.y);
}
p.endShape(p.CLOSE);
// Draw centroid
p.fill(255, 0, 0);
p.noStroke();
p.ellipse(band.centroid.x, band.centroid.y, 5 / viewScale, 5 / viewScale);
}
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MarkArtist21/shapefinder" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>