From c0362798c20c07272e82e81b38cd40ae2b7f5222 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 7 Apr 2026 15:44:12 +0800 Subject: [PATCH] feat: graphic support custom texture --- ...pport-custom-texture_2026-04-07-07-44.json | 10 ++ ...pport-custom-texture_2026-04-07-07-44.json | 10 ++ .../__tests__/graphic/texture-custom.test.ts | 89 +++++++++++++++ packages/vrender-core/src/graphic/graphic.ts | 47 ++++++-- .../vrender-core/src/interface/graphic.ts | 15 ++- .../base-texture-contribution-render.ts | 101 +++++++++++++----- .../__tests__/browser/src/pages/texture.ts | 36 ++++++- 7 files changed, 269 insertions(+), 39 deletions(-) create mode 100644 common/changes/@visactor/vrender-core/feat-vrender-support-custom-texture_2026-04-07-07-44.json create mode 100644 common/changes/@visactor/vrender/feat-vrender-support-custom-texture_2026-04-07-07-44.json create mode 100644 packages/vrender-core/__tests__/graphic/texture-custom.test.ts diff --git a/common/changes/@visactor/vrender-core/feat-vrender-support-custom-texture_2026-04-07-07-44.json b/common/changes/@visactor/vrender-core/feat-vrender-support-custom-texture_2026-04-07-07-44.json new file mode 100644 index 000000000..f7c36b03c --- /dev/null +++ b/common/changes/@visactor/vrender-core/feat-vrender-support-custom-texture_2026-04-07-07-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "feat: graphic support custom texture", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender/feat-vrender-support-custom-texture_2026-04-07-07-44.json b/common/changes/@visactor/vrender/feat-vrender-support-custom-texture_2026-04-07-07-44.json new file mode 100644 index 000000000..0536218c3 --- /dev/null +++ b/common/changes/@visactor/vrender/feat-vrender-support-custom-texture_2026-04-07-07-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender", + "comment": "feat: graphic support custom texture", + "type": "none" + } + ], + "packageName": "@visactor/vrender" +} \ No newline at end of file diff --git a/packages/vrender-core/__tests__/graphic/texture-custom.test.ts b/packages/vrender-core/__tests__/graphic/texture-custom.test.ts new file mode 100644 index 000000000..60f5e903d --- /dev/null +++ b/packages/vrender-core/__tests__/graphic/texture-custom.test.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck +import { Rect } from '../../src/graphic/rect'; +import { application } from '../../src/application'; + +describe('texture-custom (resource)', () => { + beforeAll(() => { + application.global = { + loadSvg: jest.fn(() => Promise.resolve({ data: null })), + loadImage: jest.fn(() => Promise.resolve({ data: null })), + getRequestAnimationFrame: () => (cb: () => void) => cb() + } as any; + application.graphicService = { + onAttributeUpdate: jest.fn(), + updateTempAABBBounds: jest.fn(() => ({ + x1: 0, + y1: 0, + x2: 0, + y2: 0, + clear: jest.fn(), + union: jest.fn(), + setValue: jest.fn() + })), + transformAABBBounds: jest.fn() + } as any; + }); + + it('creates resource cache when texture is HTMLCanvasElement in constructor', () => { + const canvas = document.createElement('canvas'); + const rect = new Rect({ + x: 0, + y: 0, + width: 10, + height: 10, + texture: canvas + }); + + const res = rect.resources?.get(canvas); + expect(res?.state).toBe('success'); + expect(res?.data).toBe(canvas); + }); + + it('creates resource cache when setAttribute texture is HTMLCanvasElement', () => { + const canvas = document.createElement('canvas'); + const rect = new Rect({ + x: 0, + y: 0, + width: 10, + height: 10 + }); + + rect.setAttribute('texture', canvas); + + const res = rect.resources?.get(canvas); + expect(res?.state).toBe('success'); + expect(res?.data).toBe(canvas); + }); + + it('marks resource as loading when texture is svg string', () => { + const svg = ``; + const rect = new Rect({ + x: 0, + y: 0, + width: 10, + height: 10, + texture: svg + }); + + const res = rect.resources?.get(svg); + expect(res?.state).toBe('loading'); + expect(res?.data).toBe('init'); + }); + + it('marks resource as loading when texture is image url', () => { + const url = + 'https://lf-dp.bytetos.com/obj/dp-open-internet-cn/visactor-site/bytedance/client/img/visactor/navigator-logo.svg'; + const rect = new Rect({ + x: 0, + y: 0, + width: 10, + height: 10, + texture: url + }); + + const res = rect.resources?.get(url); + expect(res?.state).toBe('loading'); + expect(res?.data).toBe('init'); + }); +}); diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index bf48f41ea..2e51aaffa 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -118,6 +118,17 @@ export const GRAPHIC_UPDATE_TAG_KEY = [ const tempConstantXYKey = ['x', 'y']; const tempConstantScaleXYKey = ['scaleX', 'scaleY']; const tempConstantAngleKey = ['angle']; +const builtinTextureTypes = new Set([ + 'circle', + 'diamond', + 'rect', + 'vertical-line', + 'horizontal-line', + 'bias-lr', + 'bias-rl', + 'grid', + 'wave' +]); const point = new Point(); @@ -259,29 +270,29 @@ export abstract class Graphic = Partial = Partial = Partial = Partial = Partial { time: BaseRenderContributionTime = BaseRenderContributionTime.afterFillStroke; useStyle: boolean = true; - textureMap?: Map; + textureMap?: Map; order: number = 10; _tempSymbolGraphic: ISymbol | null = null; @@ -236,7 +236,7 @@ export class DefaultBaseTextureRenderContribution implements IBaseRenderContribu } protected drawTexture( - texture: string, + texture: string | HTMLImageElement | HTMLCanvasElement, graphic: IGraphic, context: IContext2d, x: number, @@ -247,38 +247,50 @@ export class DefaultBaseTextureRenderContribution implements IBaseRenderContribu texturePadding: number ) { const { textureRatio = graphicAttribute.textureRatio, textureOptions = null } = graphic.attribute; - let pattern: CanvasPattern = this.textureMap.get(texture); + let pattern: CanvasPattern = null; + const patternKey = this.getPatternCacheKey(texture, textureSize, texturePadding, textureColor, context.dpr); + if (patternKey !== null) { + pattern = this.textureMap.get(patternKey); + } if (!pattern) { - switch (texture) { - case 'circle': - pattern = this.createCirclePattern(textureSize, texturePadding, textureColor, context); - break; - case 'diamond': - pattern = this.createDiamondPattern(textureSize, texturePadding, textureColor, context); - break; - case 'rect': - pattern = this.createRectPattern(textureSize, texturePadding, textureColor, context); - break; - case 'vertical-line': - pattern = this.createVerticalLinePattern(textureSize, texturePadding, textureColor, context); - break; - case 'horizontal-line': - pattern = this.createHorizontalLinePattern(textureSize, texturePadding, textureColor, context); - break; - case 'bias-lr': - pattern = this.createBiasLRLinePattern(textureSize, texturePadding, textureColor, context); - break; - case 'bias-rl': - pattern = this.createBiasRLLinePattern(textureSize, texturePadding, textureColor, context); - break; - case 'grid': - pattern = this.createGridPattern(textureSize, texturePadding, textureColor, context); - break; + if (typeof texture === 'string') { + switch (texture) { + case 'circle': + pattern = this.createCirclePattern(textureSize, texturePadding, textureColor, context); + break; + case 'diamond': + pattern = this.createDiamondPattern(textureSize, texturePadding, textureColor, context); + break; + case 'rect': + pattern = this.createRectPattern(textureSize, texturePadding, textureColor, context); + break; + case 'vertical-line': + pattern = this.createVerticalLinePattern(textureSize, texturePadding, textureColor, context); + break; + case 'horizontal-line': + pattern = this.createHorizontalLinePattern(textureSize, texturePadding, textureColor, context); + break; + case 'bias-lr': + pattern = this.createBiasLRLinePattern(textureSize, texturePadding, textureColor, context); + break; + case 'bias-rl': + pattern = this.createBiasRLLinePattern(textureSize, texturePadding, textureColor, context); + break; + case 'grid': + pattern = this.createGridPattern(textureSize, texturePadding, textureColor, context); + break; + } + } + if (!pattern) { + pattern = this.createResourcePattern(texture, graphic, context); + } + if (pattern && patternKey !== null) { + this.textureMap.set(patternKey, pattern); } } - if (textureOptions && textureOptions.dynamicTexture) { + if (typeof texture === 'string' && textureOptions && textureOptions.dynamicTexture) { // 动态纹理 const { gridConfig = {}, useNewCanvas } = textureOptions; const b = graphic.AABBBounds; @@ -396,6 +408,37 @@ export class DefaultBaseTextureRenderContribution implements IBaseRenderContribu context.restore(); } } + + protected getPatternCacheKey( + texture: string | HTMLImageElement | HTMLCanvasElement, + textureSize: number, + texturePadding: number, + textureColor: string, + dpr: number + ) { + if (typeof texture !== 'string') { + return texture; + } + if (texture === 'wave') { + return null; + } + return `${texture}-${textureSize}-${texturePadding}-${textureColor}-${dpr}`; + } + + protected createResourcePattern( + texture: string | HTMLImageElement | HTMLCanvasElement, + graphic: IGraphic, + context: IContext2d + ) { + const resource = graphic.resources?.get(texture as any); + const data = resource?.state === 'success' ? resource.data : typeof texture === 'object' ? texture : null; + if (!data) { + return null; + } + const pattern = context.createPattern(data, 'repeat'); + pattern?.setTransform && pattern.setTransform(new DOMMatrix([1 / context.dpr, 0, 0, 1 / context.dpr, 0, 0])); + return pattern; + } } export const defaultBaseTextureRenderContribution = new DefaultBaseTextureRenderContribution(); diff --git a/packages/vrender/__tests__/browser/src/pages/texture.ts b/packages/vrender/__tests__/browser/src/pages/texture.ts index 89dfc48d5..952b5884b 100644 --- a/packages/vrender/__tests__/browser/src/pages/texture.ts +++ b/packages/vrender/__tests__/browser/src/pages/texture.ts @@ -1,4 +1,4 @@ -import { createStage, createSymbol, container, createArc } from '@visactor/vrender'; +import { createStage, createSymbol, container, createArc, createRect } from '@visactor/vrender'; import { roughModule } from '@visactor/vrender-kits'; import { addShapesToStage, colorPools } from '../utils'; @@ -6,7 +6,8 @@ import { addShapesToStage, colorPools } from '../utils'; export const page = () => { const shapes = []; - + const svgTexture = ``; + const imgTexture = `https://lf-dp.bytetos.com/obj/dp-open-internet-cn/visactor-site/bytedance/client/img/visactor/navigator-logo.svg`; shapes.push( createSymbol({ x: 300, @@ -37,6 +38,37 @@ export const page = () => { }) ); + shapes.push( + createArc({ + x: 720, + y: 180, + innerRadius: 20, + outerRadius: 80, + startAngle: 0, + endAngle: Math.PI * 1.5, + fill: '#f3f5f7', + stroke: '#333', + lineWidth: 2, + texture: svgTexture + }) + ); + + // image texture 示例(图片平铺) + shapes.push( + createRect({ + x: 880, + y: 320, + width: 260, + height: 200, + fill: '#f3f5f7', + stroke: '#333', + lineWidth: 2, + texture: imgTexture, + textureSize: 80, + texturePadding: 6 + }) + ); + // shapes.push( // createArc({ // x: 700,