feat: custom image mask for wordcloud shape
- Select '自定义图片' from shape dropdown - Upload any silhouette image (black=words, white=empty) - Image preview with delete button - maskImage passed to echarts-wordcloud with keepAspect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa8e2714ca
commit
579ed2d174
|
|
@ -224,8 +224,25 @@ export function buildWordcloudOption(
|
|||
};
|
||||
});
|
||||
|
||||
// Build mask image for shape
|
||||
const maskImage = getMask(wc.shape);
|
||||
// Build mask: custom image or canvas shape
|
||||
let maskImage: HTMLCanvasElement | HTMLImageElement | null = null;
|
||||
|
||||
if (wc.shape === 'custom' && wc.customImageUrl && typeof window !== 'undefined') {
|
||||
// Use custom uploaded image as mask
|
||||
const cacheKey = 'custom-' + wc.customImageUrl.substring(0, 50);
|
||||
const cached = maskCache.get(cacheKey);
|
||||
if (cached) {
|
||||
maskImage = cached as any;
|
||||
} else {
|
||||
const img = new Image();
|
||||
img.src = wc.customImageUrl;
|
||||
maskImage = img;
|
||||
// Cache after load
|
||||
img.onload = () => { maskCache.set(cacheKey, img as any); };
|
||||
}
|
||||
} else if (wc.shape !== 'rectangle') {
|
||||
maskImage = getMask(wc.shape);
|
||||
}
|
||||
|
||||
const series: Record<string, any> = {
|
||||
type: 'wordCloud',
|
||||
|
|
@ -240,7 +257,7 @@ export function buildWordcloudOption(
|
|||
shrinkToFit: true,
|
||||
drawOutOfBound: false,
|
||||
layoutAnimation: true,
|
||||
keepAspect: false,
|
||||
keepAspect: wc.shape === 'custom',
|
||||
textStyle: {
|
||||
fontFamily: wc.fontFamily,
|
||||
fontWeight: wc.fontWeight,
|
||||
|
|
@ -255,12 +272,8 @@ export function buildWordcloudOption(
|
|||
data: wordData,
|
||||
};
|
||||
|
||||
// Apply mask for non-default shapes
|
||||
if (maskImage) {
|
||||
series.maskImage = maskImage;
|
||||
series.shape = 'circle'; // base shape, mask overrides it
|
||||
} else {
|
||||
series.shape = 'circle';
|
||||
}
|
||||
|
||||
option.series = [series];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { Select, Slider, InputNumber, Space, Typography, Radio } from 'antd';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { Select, Slider, InputNumber, Space, Typography, Radio, Button, Upload } from 'antd';
|
||||
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useChartConfig } from '@/frameworks/hooks/useChartConfig';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -14,6 +15,7 @@ const SHAPES = [
|
|||
{ label: '三角形', value: 'triangle' },
|
||||
{ label: '五角星', value: 'star' },
|
||||
{ label: '五边形', value: 'pentagon' },
|
||||
{ label: '自定义图片', value: 'custom' },
|
||||
];
|
||||
|
||||
const FONTS = [
|
||||
|
|
@ -63,13 +65,56 @@ export default function WordcloudConfig() {
|
|||
<Text strong style={{ fontSize: 12 }}>形状</Text>
|
||||
<Select
|
||||
value={config.shape}
|
||||
onChange={(val) => update({ shape: val })}
|
||||
onChange={(val) => update({ shape: val, customImageUrl: val === 'custom' ? config.customImageUrl : undefined })}
|
||||
options={SHAPES}
|
||||
size="small"
|
||||
style={{ width: '100%', marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom image upload - only when shape=custom */}
|
||||
{config.shape === 'custom' && (
|
||||
<div>
|
||||
<Text strong style={{ fontSize: 12 }}>蒙版图片</Text>
|
||||
<Text style={{ fontSize: 11, color: '#999', display: 'block', margin: '2px 0 6px' }}>
|
||||
上传黑白剪影图片(黑色区域放置文字,白色区域留空)
|
||||
</Text>
|
||||
{config.customImageUrl ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<img
|
||||
src={config.customImageUrl}
|
||||
alt="mask"
|
||||
style={{ width: 60, height: 60, objectFit: 'contain', border: '1px solid #d9d9d9', borderRadius: 4, background: '#fafafa' }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => update({ customImageUrl: undefined })}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={(file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const dataUrl = e.target?.result as string;
|
||||
if (dataUrl) update({ customImageUrl: dataUrl });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button size="small" icon={<UploadOutlined />}>上传图片</Button>
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Font Size Range */}
|
||||
<div>
|
||||
<Text strong style={{ fontSize: 12 }}>字体大小范围</Text>
|
||||
|
|
|
|||
Loading…
Reference in New Issue