diff --git a/apps/bonding-curves/app/components/curve-visualizer.tsx b/apps/bonding-curves/app/components/curve-visualizer.tsx index de87168f9..a0863e7cd 100644 --- a/apps/bonding-curves/app/components/curve-visualizer.tsx +++ b/apps/bonding-curves/app/components/curve-visualizer.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Button, Input, Label, Text } from '@0xintuition/1ui' import { generateCurvePoints } from '~/lib/curveUtils' +import html2canvas from 'html2canvas' import { X } from 'lucide-react' import { createPublicClient, @@ -1063,6 +1064,130 @@ export function CurveVisualizer() { [selectedCurveId], ) + const handleScreenshot = useCallback(async () => { + const contentArea = document.querySelector( + '.flex.flex-col.gap-4 > .flex.flex-col.gap-4.rounded-lg.border.border-border.p-4', + ) + if (!contentArea) return + + try { + const rect = contentArea.getBoundingClientRect() + const appBg = window.getComputedStyle(document.body).backgroundColor + + // Render at 8x scale + const scale = 8 + const canvas = await html2canvas(contentArea, { + backgroundColor: 'transparent', + scale: scale, + logging: false, + useCORS: true, + allowTaint: true, + foreignObjectRendering: true, + width: rect.width + scale, // scale of 1 is off by 1 px + height: rect.height + scale, + x: -rect.left, + y: -rect.top, + onclone: (clonedDoc) => { + const clonedContent = clonedDoc.querySelector( + '.flex.flex-col.gap-4 > .flex.flex-col.gap-4.rounded-lg.border.border-border.p-4', + ) + if (clonedContent) { + // Set the background color on the content area + clonedContent.style.backgroundColor = appBg + clonedContent.style.position = 'relative' + clonedContent.style.width = `${rect.width}px` + clonedContent.style.height = `${rect.height}px` + + // Fix Browse button styling + const fileInput = clonedContent.querySelector('input[type="file"]') + if (fileInput instanceof HTMLElement) { + fileInput.style.opacity = '1' + fileInput.style.cursor = 'pointer' + } + + // Ensure all child elements maintain their positions + clonedContent.querySelectorAll('*').forEach((el) => { + if (el instanceof HTMLElement) { + const originalEl = document.querySelector( + `[data-testid="${el.dataset.testid}"]`, + ) + if (originalEl) { + const originalRect = originalEl.getBoundingClientRect() + const relativeTop = originalRect.top - rect.top + const relativeLeft = originalRect.left - rect.left + el.style.position = 'absolute' + el.style.top = `${relativeTop}px` + el.style.left = `${relativeLeft}px` + } + } + }) + + // Make tooltips visible + clonedContent + .querySelectorAll('[role="tooltip"]') + .forEach((tooltip) => { + if (tooltip instanceof HTMLElement) { + tooltip.style.visibility = 'visible' + tooltip.style.opacity = '1' + tooltip.style.display = 'block' + } + }) + + // Ensure SVG elements are visible + clonedContent.querySelectorAll('svg').forEach((svg) => { + if (svg instanceof SVGElement) { + svg.style.visibility = 'visible' + svg.style.opacity = '1' + } + }) + } + }, + }) + + // Create a new canvas at 2x scale for the final output + const finalScale = 2 + const outputCanvas = document.createElement('canvas') + outputCanvas.width = rect.width * finalScale + outputCanvas.height = rect.height * finalScale + const ctx = outputCanvas.getContext('2d') + + if (ctx) { + // Enable image smoothing for better antialiasing + ctx.imageSmoothingEnabled = true + ctx.imageSmoothingQuality = 'high' + + // Draw the high-res canvas onto the smaller output canvas + ctx.drawImage( + canvas, + 0, + 0, + canvas.width, + canvas.height, + 0, + 0, + outputCanvas.width, + outputCanvas.height, + ) + } + + // Use the downscaled canvas for the blob + outputCanvas.toBlob(async (blob) => { + if (!blob) return + try { + await navigator.clipboard.write([ + new ClipboardItem({ + 'image/png': blob, + }), + ]) + } catch (err) { + console.error('Error copying to clipboard:', err) + } + }, 'image/png') + } catch (err) { + console.error('Error creating screenshot:', err) + } + }, []) + // Cleanup on unmount useEffect(() => { return () => { @@ -1074,11 +1199,9 @@ export function CurveVisualizer() { return (
-
- Bonding Curve Visualizer - - Upload and compare multiple bonding curve implementations - +
+
+
diff --git a/apps/bonding-curves/app/page.tsx b/apps/bonding-curves/app/page.tsx index 9e5b9bf69..ffdf9556f 100644 --- a/apps/bonding-curves/app/page.tsx +++ b/apps/bonding-curves/app/page.tsx @@ -2,6 +2,8 @@ import { CurveVisualizer } from './components/curve-visualizer' +// This may be unused + export default function Page() { return (
diff --git a/apps/bonding-curves/app/routes/_index.tsx b/apps/bonding-curves/app/routes/_index.tsx index 6cb27b61e..42840ec86 100644 --- a/apps/bonding-curves/app/routes/_index.tsx +++ b/apps/bonding-curves/app/routes/_index.tsx @@ -18,9 +18,12 @@ export default function Index() {
-

Bonding Curves

+

+ Intuition Bonding Curve Visualizer +

- Upload and visualize your custom bonding curve implementations + Upload and visualize any bonding curve contract compatible with + Intuition Protocol.

diff --git a/apps/bonding-curves/app/routes/api.compile.ts b/apps/bonding-curves/app/routes/api.compile.ts index 7e63e19b2..6ff9c2d00 100644 --- a/apps/bonding-curves/app/routes/api.compile.ts +++ b/apps/bonding-curves/app/routes/api.compile.ts @@ -62,11 +62,11 @@ export const action: ActionFunction = async ({ request }) => { try { // Create directories if they don't exist and set permissions - await fs.mkdir(outDir, { recursive: true, mode: 0o777 }) // Full permissions for testing - await fs.mkdir(contractsDir, { recursive: true, mode: 0o755 }) - - // Set permissions on the contract file if (isProduction) { + await fs.mkdir(outDir, { recursive: true, mode: 0o777 }) // Full permissions for testing + await fs.mkdir(contractsDir, { recursive: true, mode: 0o755 }) + + // Set permissions on the contract file await fs.writeFile(contractPath, content) await fs.chmod(contractPath, 0o644) @@ -76,14 +76,15 @@ export const action: ActionFunction = async ({ request }) => { // Log current working directory const { stdout: pwdOutput } = await execAsync('pwd') console.log('Current working directory:', pwdOutput) + + // Log contract file contents and location before compilation + console.log('Contract path:', contractPath) + console.log('Contract directory contents:', await fs.readdir(contractsDir)) } else { - await execAsync(`docker exec bonding-curves-anvil-1 bash -c "echo '${escapedContent}' > ${contractPath}"`) + // In development, only create directories in Docker + await execAsync(`docker exec bonding-curves-anvil-1 bash -c "mkdir -p /app/contracts /app/out && echo '${escapedContent}' > /app/contracts/${fileName}"`) } - // Log contract file contents and location before compilation - console.log('Contract path:', contractPath) - console.log('Contract directory contents:', await fs.readdir(contractsDir)) - let compileResult: { stdout: string; stderr: string } if (isProduction) { console.log('Running forge build...') @@ -117,8 +118,10 @@ export const action: ActionFunction = async ({ request }) => { } // Log directory contents and permissions for debugging - const { stdout: lsOutput } = await execAsync(`ls -la ${outDir}`) - console.log('Output directory permissions:', lsOutput) + if (isProduction) { + const { stdout: lsOutput } = await execAsync(`ls -la ${outDir}`) + console.log('Output directory permissions:', lsOutput) + } console.log('Compilation output:', compileResult.stdout) if (compileResult.stderr) { diff --git a/apps/bonding-curves/package.json b/apps/bonding-curves/package.json index a2727784c..c1e284edf 100644 --- a/apps/bonding-curves/package.json +++ b/apps/bonding-curves/package.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "d3": "^7.8.5", "framer-motion": "^11.2.10", + "html2canvas": "^1.4.1", "isbot": "^4", "lucide-react": "^0.441.0", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef6876e05..369c9b18e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: framer-motion: specifier: ^11.2.10 version: 11.2.10(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 isbot: specifier: ^4 version: 4.4.0 @@ -8136,6 +8139,10 @@ packages: base-x@4.0.0: resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@0.0.8: resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} engines: {node: '>= 0.4'} @@ -8813,6 +8820,9 @@ packages: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} @@ -10602,6 +10612,10 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -14758,6 +14772,9 @@ packages: text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -15391,6 +15408,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -26638,6 +26658,8 @@ snapshots: base-x@4.0.0: {} + base64-arraybuffer@1.0.2: {} + base64-js@0.0.8: {} base64-js@1.5.1: {} @@ -27416,6 +27438,10 @@ snapshots: css-color-keywords@1.0.0: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@5.1.0: dependencies: boolbase: 1.0.0 @@ -29820,6 +29846,11 @@ snapshots: html-tags@3.3.1: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http-cache-semantics@4.1.1: {} http-errors@2.0.0: @@ -35156,6 +35187,10 @@ snapshots: text-encoding-utf-8@1.0.2: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -35777,6 +35812,10 @@ snapshots: utils-merge@1.0.1: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@8.3.2: {} uuid@9.0.1: {}