展示类

图片裁切

通过设定裁切框的宽高比例,自由裁切图片

使用场景

Cropper 用于裁切图片,支持自定义裁切框样式,可通过拖动调整裁切框位置,被裁切图片位置;可缩放,旋转被裁切图片。

代码演示

如何引入

import { Cropper } from '@fibrous/semi-ui-solid';

基本用法

通过 src 设置被裁切的图片; 可通过 shape 设置裁切框形状,默认为方形。



代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Radio, RadioGroup, type CropperElement, type CropperProps, type RadioChangeEvent } from '@fibrous/semi-ui-solid';

const containerStyle = {
    width: '550px',
    height: '300px',
    margin: '20px',
};

export default function CropperDemo() {
    let cropper: CropperElement | undefined;
    const [shape, setShape] = createSignal<CropperProps['shape']>('rect');
    const [cropperUrl, setCropperUrl] = createSignal('');

    const onButtonClick = () => {
        const canvas = cropper?.getCropperCanvas();
        if (canvas) setCropperUrl(canvas.toDataURL());
    };

    const onShapeChange = (event: RadioChangeEvent) => {
        setShape(event.target.value);
    };

    return (
        <>
            <RadioGroup aria-label="裁切框形状" onChange={onShapeChange} value={shape()}>
                <Radio value="rect">rect</Radio>
                <Radio value="round">round</Radio>
                <Radio value="roundRect">roundRect</Radio>
            </RadioGroup>
            <Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} shape={shape()} />
            <Button onClick={onButtonClick}>裁切</Button>
            <br />
            <br />
            <Show when={cropperUrl()}>
                <img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
            </Show>
        </>
    );
}

自定义裁切框比例

可通过 defaultAspectRatio 初始的裁切框比例(默认为 1)。可通过 aspectRatio 设置固定的裁切框比例。

设置 defaultAspectRatio仅对初始的裁切框比例生效, 拖动时,裁切框比例会随着拖动而变化。

设置 aspectRatio 时,裁切框比例固定,拖动时将裁切框将以此比例变化。图片加载后改变 aspectRatio,裁切框会按新比例重排:保持高度,宽度随比例变化,超出容器时缩到容器内,中心尽量保持不动;aspectRatio 变为 undefined 时不重排。



代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';

const containerStyle = {
    width: '550px',
    height: '300px',
    margin: '20px',
};

export default function CropperDemo() {
    let cropper: CropperElement | undefined;
    const [cropperUrl, setCropperUrl] = createSignal('');

    const onButtonClick = () => {
        const canvas = cropper?.getCropperCanvas();
        if (canvas) setCropperUrl(canvas.toDataURL());
    };

    return (
        <>
            <Cropper aspectRatio={3 / 4} ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} />
            <Button onClick={onButtonClick}>裁切</Button>
            <br />
            <br />
            <Show when={cropperUrl()}>
                <img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
            </Show>
        </>
    );
}

受控旋转/缩放图片

通过 rotate 和 zoom 控制图片旋转和缩放,可通过 onZoomChange 拿到最新的 zoom 值。rotate 为单向绑定,组件不会改变它。滚轮缩放时会调用 onZoomChange,父组件需在回调中更新 zoom,否则缩放会被还原为受控的值。

旋转
-360360
缩放
0.13



代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';

const containerStyle = {
    width: '550px',
    height: '300px',
    margin: '20px',
};

const actionStyle = {
    'margin-top': '20px',
    display: 'flex',
    'align-items': 'center',
    'justify-content': 'center',
    width: 'fit-content',
};

export default function CropperDemo() {
    let cropper: CropperElement | undefined;
    const [rotate, setRotate] = createSignal(0);
    const [zoom, setZoom] = createSignal(1);
    const [cropperUrl, setCropperUrl] = createSignal('');

    const onSliderChange = (value?: number | number[]) => {
        if (typeof value === 'number') setRotate(value);
    };

    const onZoomSliderChange = (value?: number | number[]) => {
        if (typeof value === 'number') setZoom(value);
    };

    const onButtonClick = () => {
        const canvas = cropper?.getCropperCanvas();
        if (canvas) setCropperUrl(canvas.toDataURL());
    };

    return (
        <div id="cropper-container">
            <Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} />
            <div style={actionStyle}>
                <span id="cropper-rotate-zoom-rotate">旋转</span>
                <Slider aria-labelledby="cropper-rotate-zoom-rotate" style={{ width: '500px' }} value={rotate()} step={1} min={-360} max={360} onChange={onSliderChange} />
            </div>
            <div style={actionStyle}>
                <span id="cropper-rotate-zoom-zoom">缩放</span>
                <Slider aria-labelledby="cropper-rotate-zoom-zoom" style={{ width: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
            </div>
            <br />
            <Button onClick={onButtonClick}>裁切</Button>
            <br />
            <br />
            <Show when={cropperUrl()}>
                <img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
            </Show>
        </div>
    );
}

裁切框设置

可通过 cropperBoxStyle, cropperBoxCls 自定义裁切框样式。可通过 showResizeBox 设置是否展示裁切框边角的调整块。

showResizeBox = false,并修改边框颜色


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';

const containerStyle = {
    width: '550px',
    height: '300px',
    margin: '20px',
};

export default function CropperDemo() {
    let cropper: CropperElement | undefined;
    const [cropperUrl, setCropperUrl] = createSignal('');

    const onButtonClick = () => {
        const canvas = cropper?.getCropperCanvas();
        if (canvas) setCropperUrl(canvas.toDataURL());
    };

    return (
        <>
            <strong>showResizeBox = false,并修改边框颜色</strong>
            <Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} cropperBoxStyle={{ 'outline-color': 'var(--semi-color-bg-0)' }} showResizeBox={false} />
            <Button onClick={onButtonClick}>裁切</Button>
            <br />
            <br />
            <Show when={cropperUrl()}>
                <img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
            </Show>
        </>
    );
}

实时预览裁切效果

通过 preview 指定预览容器,实时预览裁切效果。preview 是返回容器 DOM 的函数,容器尚未挂载时可返回 null。

旋转
-360360
缩放
0.13

实时预览


代码
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';

const containerStyle = {
    width: '550px',
    height: '300px',
    margin: '20px',
};

const actionStyle = {
    'margin-top': '20px',
    display: 'flex',
    'align-items': 'center',
    'justify-content': 'center',
    width: 'fit-content',
};

export default function CropperDemo() {
    let cropper: CropperElement | undefined;
    let previewWrapper: HTMLDivElement | undefined;
    const [rotate, setRotate] = createSignal(0);
    const [zoom, setZoom] = createSignal(1);
    const [cropperUrl, setCropperUrl] = createSignal('');

    const onSliderChange = (value?: number | number[]) => {
        if (typeof value === 'number') setRotate(value);
    };

    const onZoomSliderChange = (value?: number | number[]) => {
        if (typeof value === 'number') setZoom(value);
    };

    const onButtonClick = () => {
        const canvas = cropper?.getCropperCanvas();
        if (canvas) setCropperUrl(canvas.toDataURL());
    };

    return (
        <div>
            <Cropper ref={(node) => (cropper = node)} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} preview={() => previewWrapper ?? null} src="/assets/root-web-sites/abstract.jpg" style={containerStyle} />
            <div style={actionStyle}>
                <span id="cropper-preview-rotate">旋转</span>
                <Slider aria-labelledby="cropper-preview-rotate" style={{ width: '500px' }} value={rotate()} step={1} min={-360} max={360} onChange={onSliderChange} />
            </div>
            <div style={actionStyle}>
                <span id="cropper-preview-zoom">缩放</span>
                <Slider aria-labelledby="cropper-preview-zoom" style={{ width: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
            </div>
            <br />
            <div style={{ display: 'flex' }}>
                <div style={{ width: '50%', 'flex-grow': '1' }}>
                    <strong>实时预览</strong>
                    <div ref={(node) => (previewWrapper = node)} style={{ height: '300px', 'margin-top': '8px' }} />
                </div>
                <div style={{ width: '50%', 'flex-grow': '1', 'padding-left': '10px' }}>
                    <Button onClick={onButtonClick}>裁切</Button>
                    <br />
                    <br />
                    <Show when={cropperUrl()}>
                        <img src={cropperUrl()} alt="裁切结果" style={{ width: '90%' }} />
                    </Show>
                </div>
            </div>
        </div>
    );
}

API

Props

属性说明类型默认值
aspectRatio固定的裁切框宽高比;加载后改变时,裁切框按新比例重排。number-
cropperBoxCls裁切框类名。string-
cropperBoxStyle裁切框样式,与组件计算的宽高和位移合并,宽高与位移以组件为准。JSX.CSSProperties-
defaultAspectRatio裁切框的初始宽高比,拖动时会随之变化。number1
fill裁切结果中非图片部分的填充色。string"rgba(0, 0, 0, 0)"
imgProps传给被裁切图片 img 标签的属性。src 与布局样式由组件决定,class 与组件自身的类名合并,style 位于组件布局之下(宽高与位移以组件为准),onLoad 在组件处理完成后调用;alt 默认为空字符串,crossorigin 默认为 anonymous,均可覆盖。CropperImgProps-
maxZoom最大缩放倍数。number3
minZoom最小缩放倍数。number0.1
onZoomChange滚轮缩放时触发,参数为新的缩放比例。Type(zoom: number) => void-
preview返回预览容器 DOM 的函数,裁切效果会实时镜像到该容器;可返回 null,表示容器尚未挂载。Type() => HTMLElement | null-
ref根 div 元素,其上附加 getCropperCanvas 方法。TypeRef<CropperElement>-
rotate图片旋转角度,单向绑定,组件不会改变它。number-
shape裁切框形状。'rect' | 'round' | 'roundRect'"rect"
showResizeBox是否展示裁切框边角的调整块。booleantrue
src被裁切图片的地址。string-
zoom受控的缩放比例,叠加在图片适应裁切区域的初始缩放之上。number-
zoomStep滚轮缩放的步长。number0.1

Methods

方法说明类型
getCropperCanvas将裁切框内的区域按图片自身的分辨率绘制到新的 canvas 上。Type() => HTMLCanvasElement

class、style、ref 与原生属性和事件(如 id、data-*、aria-*、onKeyDown)落在根 <div> 上;ref 拿到的就是根元素,其上附加了 getCropperCanvas 方法。

Accessibility

  • 根元素传入 aria-label 或 aria-labelledby 且未传 role 时,自动带 role="group";传入的 role 优先
  • 裁切框是可聚焦的命名分组(role="group",名称与角色描述取自语言包,并通过 aria-describedby 指向操作提示),图片加载完成后才可聚焦。被裁切的 img 是装饰性的,alt 默认为空字符串,可经 imgProps 覆盖
  • 只有主按钮的鼠标按下,以及 touch、pen 触点,才开始拖动裁切框、调整块和图片;鼠标中键、右键按下不会开始拖动

键盘和焦点

  • 裁切框是组件内唯一的 Tab 停靠点,图片加载完成后才可聚焦
  • 焦点在裁切框上时,方向键每次移动裁切框 10px,不会移出容器
  • Shift + 方向键每次移动裁切框右下角 10px 来调整大小,限制在容器内;设置了 aspectRatio 时保持该比例(横向的变化换算为高度)。仅在 showResizeBox 为 true(有调整块)时可用,为 false 时 Shift + 方向键不起作用
  • 缩放与旋转没有内置的键盘操作,需要使用方提供可用键盘操作的控件(如示例中的 Slider)

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$color-cropper_mask-bgvar(--semi-color-overlay-bg)
$color-cropper_box-outlinevar(--semi-color-primary)
$color-cropper_box_corner-bgvar(--semi-color-primary)
$width-cropper_box-outline1px
$width-cropper_box_corner10px