import React, { useCallback, useEffect, useRef } from 'react'; import { fabric } from 'fabric'; import { loadFont } from 'docs/utils'; import { Warpvas } from 'warpvas'; import WarpvasPerspective from 'warpvas-perspective'; const CANVAS_PADDING = 8; const LOGO_WIDTH = 460; const LOGO_HEIGHT = 150; const INDICATOR_THEME_COLOR = 'rgba(206, 102, 91, 1)'; const INDICATOR_SUB_THEME_COLOR = 'rgba(206, 102, 91, 0.6)'; const createLOGO = () => { const width = LOGO_WIDTH; const height = LOGO_HEIGHT; const background = new fabric.Rect({ width: width, height: height, left: width / 2, fill: 'transparent', originX: 'center', }); const title = new fabric.Text('Warpvas', { left: width / 2, top: 24, fontSize: 50, fontFamily: 'sharpie-black', originX: 'center', originY: 'top', }); const description = new fabric.Text('A JavaScript Library for Rapid Canvas Distortion', { left: width / 2, top: 126, fontSize: 16, fontFamily: 'handwriting', originX: 'center', originY: 'bottom', }); const group = new fabric.Group([background, title, description], { originX: 'center', originY: 'center', }); return group.toCanvasElement({ enableRetinaScaling: true }); }; const createPath = (path: string, options: fabric.IPathOptions = {}) => { return new fabric.Path(path, { fill: 'transparent', stroke: INDICATOR_SUB_THEME_COLOR, strokeWidth: 1, strokeDashArray: [2, 2], ...options, }); }; const createLine = (p0: Coord, p1: Coord, options: fabric.ILineOptions = {}) => { return new fabric.Line([p0.x, p0.y, p1.x, p1.y], { stroke: INDICATOR_SUB_THEME_COLOR, strokeWidth: 1, ...options, }); }; const createPoint = (options: fabric.ICircleOptions = {}) => { return new fabric.Circle({ radius: 4, fill: INDICATOR_THEME_COLOR, stroke: INDICATOR_SUB_THEME_COLOR, strokeWidth: 6, originX: 'center', originY: 'center', ...options, }); }; const createRect = (width: number, height: number, options: fabric.IRectOptions = {}) => { return new fabric.Rect({ width, height, fill: 'transparent', stroke: INDICATOR_SUB_THEME_COLOR, strokeWidth: 1, strokeDashArray: [2, 2], ...options, }); }; const createLabel = (text: string, options: fabric.TextOptions = {}) => { return new fabric.Text(text, { fontSize: 12, fontFamily: 'handwriting', fill: INDICATOR_THEME_COLOR, originX: 'center', originY: 'top', ...options, }); }; const WarpedCanvas: React.FC<{ className?: string; step: number }> = ({ className, step }) => { const canvasRef = useRef(null); const fabricCanvasRef = useRef(); const initialWarpedCanvas = useCallback(async (step: number) => { if (!canvasRef.current) return; if (fabricCanvasRef.current) { fabricCanvasRef.current.dispose(); } await loadFont('sharpie-black'); await loadFont('handwriting'); const offset = (...objects: fabric.Object[]) => { objects.map((object) => { (object as any).set({ left: object.left! + CANVAS_PADDING, top: object.top! + CANVAS_PADDING, }); }); return objects; }; const applyDistortionEffects = (warpvas: Warpvas, step: number) => { const ratio = window.devicePixelRatio; switch (step) { case 2: { warpvas.updateVertexCoord(0, 0, 'tl', { x: 50 * ratio, y: 50 * ratio }, true); break; } case 3: { warpvas.updateRegionBoundCoords(0, 0, 'bottom', [ { x: 0, y: LOGO_HEIGHT * ratio }, { x: (LOGO_WIDTH * ratio) / 3, y: (LOGO_HEIGHT * ratio) / 2 }, { x: ((LOGO_WIDTH * ratio) / 3) * 2, y: (LOGO_HEIGHT * ratio) / 2 }, { x: LOGO_WIDTH * ratio, y: LOGO_HEIGHT * ratio }, ]); break; } case 4: { const region = warpvas.originalRegions[0][0]; warpvas.splitRegionByPoint( 0, 0, { x: (region.tl.x + region.br.x) / 2, y: (region.tl.y + region.br.y) / 2, }, 0.1, ); break; } case 6: { warpvas.setRenderingConfig({ enableGridDisplay: true, enableGridVertexDisplay: true, gridColor: { r: 206, g: 102, b: 91, a: 1 }, }); break; } case 7: { warpvas.setSplitUnit(0.02).setRenderingConfig({ enableGridDisplay: true, gridColor: { r: 206, g: 102, b: 91, a: 1 }, }); break; } case 8: { warpvas.updateVertexCoord(0, 0, 'tl', { x: 0, y: 100 * ratio }, true); warpvas.setSplitStrategy(WarpvasPerspective); warpvas.setRenderingConfig({ enableGridDisplay: true, gridColor: { r: 206, g: 102, b: 91, a: 1 }, }); break; } case 9: { warpvas.setInputLimitSize({ height: 100 * ratio, }); break; } case 10: { warpvas.setOutputLimitSize({ height: 100 * ratio, }); break; } case 11: { warpvas.setRenderingContext('2d'); break; } } }; const addIndicators = (canvas: fabric.StaticCanvas, step: number) => { switch (step) { case 2: { const path = createPath(`M 0 ${LOGO_HEIGHT} L 50 50 L ${LOGO_WIDTH} 0`); const point = createPoint({ left: 50, top: 50, }); const text = createLabel('Move to (50, 50)', { left: 50, top: 50 + 20, }); canvas.add(...offset(path, point, text)); break; } case 3: { const curve = [ { x: 0, y: LOGO_HEIGHT }, { x: LOGO_WIDTH / 3, y: LOGO_HEIGHT / 2 }, { x: (LOGO_WIDTH / 3) * 2, y: LOGO_HEIGHT / 2 }, { x: LOGO_WIDTH, y: LOGO_HEIGHT }, ]; const path = createPath( `M 0 ${LOGO_HEIGHT} C ${curve .slice(1) .map((i) => `${i.x} ${i.y}`) .join(' ')}`, ); const lines = [ [curve[0], curve[1]], [curve[2], curve[3]], ].map(([p0, p1]) => { return createLine(p0, p1); }); const points = curve.map((pos) => { return createPoint({ left: pos.x, top: pos.y, }); }); canvas.add(...offset(path, ...lines, ...points)); break; } case 4: { const path = createPath( `M 0 ${LOGO_HEIGHT / 2} L ${LOGO_WIDTH} ${LOGO_HEIGHT / 2} M ${LOGO_WIDTH / 2} 0 L ${LOGO_WIDTH / 2} ${LOGO_HEIGHT}`, ); const point = createPoint({ left: LOGO_WIDTH / 2, top: LOGO_HEIGHT / 2, }); const text = createLabel('Split at center', { left: LOGO_WIDTH / 2, top: LOGO_HEIGHT / 2 + 20, }); canvas.add(...offset(path, point, text)); break; } case 5: { const rect1 = createRect(LOGO_WIDTH / 2, LOGO_HEIGHT, { left: LOGO_WIDTH / 2, top: 0, }); const rect2 = createRect(LOGO_WIDTH, LOGO_HEIGHT / 2, { left: 0, top: LOGO_HEIGHT / 2, }); const text = createLabel('Remove region at row 1, column 1', { left: LOGO_WIDTH / 2, top: LOGO_HEIGHT / 2 + 10, }); canvas.add(...offset(rect1, rect2, text)); break; } case 9: case 10: { const path = createPath(`M 0 100 L 0 0`); const text = createLabel('Limit height to 100px', { left: 10, top: 100 / 2, originX: 'left', }); canvas.add(...offset(path, text)); break; } } }; // Generate pre-deformation canvas const logoCanvas = createLOGO(); // Apply deformation based on steps const warpvas = new Warpvas(logoCanvas); applyDistortionEffects(warpvas, step); const warpedCanvas = warpvas.render(); const image = new fabric.Image(warpedCanvas, { originX: 'center', originY: 'center', scaleX: 1 / window.devicePixelRatio, scaleY: 1 / window.devicePixelRatio, }); // Generate preview canvas const canvas = canvasRef.current; const fabricCanvas = new fabric.StaticCanvas(canvas, { width: image.getScaledWidth() + CANVAS_PADDING * 2, height: image.getScaledHeight() + CANVAS_PADDING * 2, }); image.set({ left: fabricCanvas.getWidth() / 2, top: fabricCanvas.getHeight() / 2, }); fabricCanvas.add(image); // Add hint elements addIndicators(fabricCanvas, step); fabricCanvasRef.current = fabricCanvas; }, []); useEffect(() => { initialWarpedCanvas(step); }, [step]); return (
); }; export default WarpedCanvas;