SVG
Quadratic-bezier
svg-tutorial 中这个例子很漂亮,尝试复刻一下。
点击展开源码
vue
<script setup lang="tsx">
import React, { useState } from 'react';
import ReactWrap from '../ReactWrap.vue';
import { useRef } from 'react';
import clsx from 'clsx';
const DraggableIcon: React.FC<{ svgRef: React.MutableRefObject<SVGSVGElement | null>, pos: [number, number], setPos: React.Dispatch<React.SetStateAction<[number, number]>> }> = ({ svgRef, pos, setPos }) => {
const draggingHandler = useRef<any>();
return <g
className={clsx('draggable', { dragging: !!draggingHandler.current })}
transform={`translate(${pos[0]},${pos[1]})`}
onDragStart={e => e.preventDefault()}
onMouseDown={() => {
if (draggingHandler.current) document.removeEventListener('mousemove', draggingHandler.current);
draggingHandler.current = (e: React.MouseEvent<SVGGElement>) => {
const rect = svgRef.current!.getBoundingClientRect()
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (x < 0 || y < 0 || e.clientX > rect.right || e.clientY > rect.bottom) return;
setPos([Math.round(x), Math.round(y)]);
}
document.addEventListener('mousemove', draggingHandler.current);
}}
onMouseUp={(e) => {
const forceUpdate = draggingHandler.current;
document.removeEventListener('mousemove', draggingHandler.current);
draggingHandler.current = null;
forceUpdate?.(e);
}}
>
<circle x="-20" y="-20" r="20"></circle>
<line className="arrowLine" x1="-7.5" y1="0" x2="7.5" y2="0"></line>
<line className="arrowLine" x1="0" y1="-7.5" x2="0" y2="7.5"></line>
<g transform="translate(0, -10)">
<polygon className="arrowHead" points="0,0 -3,5 +3,5" transform="rotate(0)"></polygon>
</g>
<g transform="translate(0, 10)">
<polygon className="arrowHead" points="0,0 -3,5 +3,5" transform="rotate(180)"></polygon>
</g>
<g transform="translate(-10, 0)">
<polygon className="arrowHead" points="0,0 -3,5 +3,5" transform="rotate(270)"></polygon>
</g>
<g transform="translate(10, 0)">
<polygon className="arrowHead" points="0,0 -3,5 +3,5" transform="rotate(90)"></polygon>
</g>
<text x="20" y="-20" textAnchor="left" stroke="none">{pos[0]}, {pos[1]}</text>
</g>
}
const app: React.FC = () => {
const svgRef = useRef<SVGSVGElement | null>(null);
const [leftPos, setLeftPos] = useState<[number, number]>([100, 350]);
const [rightPos, setRightPos] = useState<[number, number]>([350, 350]);
const [ctrlPos, setCtrlPos] = useState<[number, number]>([225, 50])
return <svg ref={svgRef} width="450" height="450">
<path d={`M ${leftPos[0]} ${leftPos[1]} Q ${ctrlPos[0]} ${ctrlPos[1]}, ${rightPos[0]} ${rightPos[1]} `} stroke="red" strokeWidth="20" fill="none" />
<line className="presentationHelper" x1={`${leftPos[0]} `} y1={`${leftPos[1]} `} x2={`${ctrlPos[0]} `} y2={`${ctrlPos[1]} `}></line>
<line className="presentationHelper" x1={`${rightPos[0]} `} y1={`${rightPos[1]} `} x2={`${ctrlPos[0]} `} y2={`${ctrlPos[1]} `}></line>
<DraggableIcon svgRef={svgRef} pos={leftPos} setPos={setLeftPos} />
<DraggableIcon svgRef={svgRef} pos={rightPos} setPos={setRightPos} />
<DraggableIcon svgRef={svgRef} pos={ctrlPos} setPos={setCtrlPos} />
</svg>
}
</script>
<template>
<ReactWrap :app="app" class="svg-demo" />
</template>
<style lang="css">
.svg-demo {
svg {
border: 1px solid #d5d5d5;
user-select: none;
}
.draggable {
fill: hsla(0, 0%, 82%, .585);
cursor: grab;
&:hover {
fill: rgba(96, 0, 30, .8);
.arrowHead {
fill: white;
}
.arrowLine {
stroke: white;
}
}
.arrowHead {
fill: dimgray;
}
.arrowLine {
stroke-width: 2px;
stroke: dimgray;
}
}
.presentationHelper {
stroke: dimgray;
stroke-width: 1px;
stroke-dasharray: 20, 20;
fill: none;
}
.dragging {
fill: rgba(193, 0, 61, .8);
cursor: grabbing;
}
}
</style>
foreignObject
SVG 中有一个foreignObject
,理论上可以放置各种HTML元素,因此可以在一些静态场景用于“快照”截图,或者在SVG驱动的绘图引擎中与框架组件集成。这个例子中,第一个按钮是真实的按钮,其他的都是“快照”:
点击展开源码
tsx
import React, { HTMLAttributes, useCallback, useEffect, useRef } from "react";
const serializer = new XMLSerializer();
export default (name: string) => {
const From: React.FC<{ render: (props: any) => React.ReactNode }> = ({ render }) => {
const ref = useRef<any>();
const snapshot = useCallback((e: Event) => {
const el = ref.current;
if (!el) return;
const clone = el.cloneNode(true);
const styles = window.getComputedStyle(el);
for (let style of styles) {
clone.style[style] = styles[style];
}
const toIndex = (e as CustomEvent).detail;
document.dispatchEvent(new CustomEvent(`${name}_${toIndex}`, {
detail: serializer.serializeToString(clone)
}))
}, []);
useEffect(() => {
document.addEventListener(`${name}_snap`, snapshot);
return () => document.removeEventListener(`${name}_snap`, snapshot);
}, []);
return render({ ref });
}
let toIndex = 0;
const To: React.FC<{ dependencies?: any[] } & HTMLAttributes<SVGElement>> = ({ dependencies = [], ...rest }) => {
const ref = useRef<SVGForeignObjectElement>(null);
useEffect(() => {
++toIndex;
const handle = (e: Event) => {
if (!ref.current) return;
ref.current.innerHTML = (e as CustomEvent).detail;
}
document.addEventListener(`${name}_${toIndex}`, handle);
return () => document.removeEventListener(`${name}_${toIndex}`, handle);
}, []);
useEffect(() => {
document.dispatchEvent(new CustomEvent(`${name}_snap`, {
detail: toIndex // only notify myself
}));
}, dependencies);
return <svg xmlns="http://www.w3.org/2000/svg" {...rest}>
<foreignObject ref={ref} x="0" y="0" width="100%" height="100%" />
</svg>
};
return { From, To }
}
vue
<script setup lang="tsx">
import React, { useMemo, useState } from 'react';
import { h } from 'vue';
import Button from '../Button.vue';
import ReactWrap from '../ReactWrap.vue';
import createSnapshot from '../Snapshot.tsx';
import VueWrap from '../VueWrap.tsx';
const Snapshot = createSnapshot("demo");
const app = () => {
const [count, setCount] = useState(0);
const btn = useMemo(() => ({
setup() {
return () => h(
Button,
{
onClick() {
setCount(count + 1);
}
},
() => `Clicked ${count} times`
)
}
}), [count])
return <>
<Snapshot.From
render={inject => {
return <VueWrap app={btn} {...inject} />
}}
/>
<Snapshot.To />
<Snapshot.To dependencies={[count]} />
</>
}
</script>
<template>
<ReactWrap :app="app" />
</template>