|
|
@@ -1,6 +1,8 @@
|
|
|
import {
|
|
|
Box2,
|
|
|
BufferGeometry,
|
|
|
+ CanvasTexture,
|
|
|
+ ClampToEdgeWrapping,
|
|
|
Color,
|
|
|
DoubleSide,
|
|
|
FileLoader,
|
|
|
@@ -8,7 +10,9 @@ import {
|
|
|
Loader,
|
|
|
Matrix3,
|
|
|
MeshBasicMaterial,
|
|
|
+ MirroredRepeatWrapping,
|
|
|
Path,
|
|
|
+ RepeatWrapping,
|
|
|
Shape,
|
|
|
ShapePath,
|
|
|
ShapeUtils,
|
|
|
@@ -147,6 +151,12 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
if ( node.nodeType !== 1 ) return;
|
|
|
|
|
|
+ if ( node.hasAttribute( 'filter' ) ) {
|
|
|
+
|
|
|
+ console.warn( 'THREE.SVGLoader: Filters are not supported.' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
const transform = getNodeTransform( node );
|
|
|
|
|
|
let isDefsNode = false;
|
|
|
@@ -231,7 +241,7 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
if ( path ) {
|
|
|
|
|
|
- if ( style.fill !== undefined && style.fill !== 'none' ) {
|
|
|
+ if ( style.fill !== undefined && style.fill !== 'none' && ! style.fill.startsWith( 'url' ) ) {
|
|
|
|
|
|
path.color.setStyle( style.fill, COLOR_SPACE_SVG );
|
|
|
|
|
|
@@ -244,7 +254,7 @@ class SVGLoader extends Loader {
|
|
|
const pathStyle = Object.assign( {}, style );
|
|
|
pathStyle.strokeWidth = style.strokeWidth * getTransformScale( currentTransform );
|
|
|
|
|
|
- path.userData = { node: node, style: pathStyle };
|
|
|
+ path.userData = { node: node, style: pathStyle, transform: currentTransform.clone(), gradients: gradients };
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -1050,6 +1060,146 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
//
|
|
|
|
|
|
+ function parseGradients( xml ) {
|
|
|
+
|
|
|
+ const HREF_NS = 'http://www.w3.org/1999/xlink';
|
|
|
+ const gradientNodes = xml.querySelectorAll( 'linearGradient, radialGradient' );
|
|
|
+ const ATTRS = [ 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'fx', 'fy', 'gradientUnits', 'gradientTransform', 'spreadMethod' ];
|
|
|
+
|
|
|
+ const parsed = {};
|
|
|
+
|
|
|
+ for ( const node of gradientNodes ) {
|
|
|
+
|
|
|
+ const id = node.getAttribute( 'id' );
|
|
|
+ if ( ! id ) continue;
|
|
|
+
|
|
|
+ const entry = {
|
|
|
+ type: node.nodeName === 'radialGradient' ? 'radialGradient' : 'linearGradient',
|
|
|
+ attrs: {},
|
|
|
+ stops: null,
|
|
|
+ href: null,
|
|
|
+ };
|
|
|
+
|
|
|
+ const href = node.getAttributeNS( HREF_NS, 'href' ) || node.getAttribute( 'href' ) || '';
|
|
|
+ if ( href.startsWith( '#' ) ) entry.href = href.substring( 1 );
|
|
|
+
|
|
|
+ for ( const name of ATTRS ) {
|
|
|
+
|
|
|
+ if ( node.hasAttribute( name ) ) entry.attrs[ name ] = node.getAttribute( name );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const stopNodes = node.querySelectorAll( 'stop' );
|
|
|
+ if ( stopNodes.length > 0 ) {
|
|
|
+
|
|
|
+ entry.stops = [];
|
|
|
+ for ( const s of stopNodes ) {
|
|
|
+
|
|
|
+ let color = s.getAttribute( 'stop-color' );
|
|
|
+ if ( ! color && s.style ) color = s.style[ 'stop-color' ];
|
|
|
+ if ( ! color ) color = '#000';
|
|
|
+
|
|
|
+ let opacity = s.getAttribute( 'stop-opacity' );
|
|
|
+ if ( ( opacity === null || opacity === '' ) && s.style ) opacity = s.style[ 'stop-opacity' ];
|
|
|
+ opacity = ( opacity === null || opacity === '' || opacity === undefined )
|
|
|
+ ? 1
|
|
|
+ : Math.max( 0, Math.min( 1, parseFloat( opacity ) ) );
|
|
|
+
|
|
|
+ const offset = Math.max( 0, Math.min( 1, parseFloat( s.getAttribute( 'offset' ) || '0' ) ) );
|
|
|
+ entry.stops.push( { offset, color, opacity } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ parsed[ id ] = entry;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function inherit( id, visited ) {
|
|
|
+
|
|
|
+ const entry = parsed[ id ];
|
|
|
+ if ( ! entry || visited.has( id ) ) return entry;
|
|
|
+ visited.add( id );
|
|
|
+
|
|
|
+ if ( entry.href && parsed[ entry.href ] ) {
|
|
|
+
|
|
|
+ const parent = inherit( entry.href, visited );
|
|
|
+ if ( parent ) {
|
|
|
+
|
|
|
+ if ( ! entry.stops ) entry.stops = parent.stops;
|
|
|
+ for ( const key in parent.attrs ) {
|
|
|
+
|
|
|
+ if ( ! ( key in entry.attrs ) ) entry.attrs[ key ] = parent.attrs[ key ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return entry;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ for ( const id in parsed ) inherit( id, new Set() );
|
|
|
+
|
|
|
+ for ( const id in parsed ) {
|
|
|
+
|
|
|
+ const entry = parsed[ id ];
|
|
|
+ const a = entry.attrs;
|
|
|
+ const units = a.gradientUnits === 'userSpaceOnUse' ? 'userSpaceOnUse' : 'objectBoundingBox';
|
|
|
+
|
|
|
+ const gradient = {
|
|
|
+ type: entry.type,
|
|
|
+ gradientUnits: units,
|
|
|
+ spreadMethod: a.spreadMethod === 'reflect' || a.spreadMethod === 'repeat' ? a.spreadMethod : 'pad',
|
|
|
+ gradientTransform: null,
|
|
|
+ stops: ( entry.stops || [] ).slice().sort( ( x, y ) => x.offset - y.offset ),
|
|
|
+ };
|
|
|
+
|
|
|
+ if ( a.gradientTransform ) {
|
|
|
+
|
|
|
+ gradient.gradientTransform = new Matrix3();
|
|
|
+ parseTransformString( a.gradientTransform, gradient.gradientTransform );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function coord( str ) {
|
|
|
+
|
|
|
+ if ( typeof str !== 'string' ) return 0;
|
|
|
+ if ( str.endsWith( '%' ) ) return parseFloat( str ) / 100;
|
|
|
+ return parseFloatWithUnits( str );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( entry.type === 'linearGradient' ) {
|
|
|
+
|
|
|
+ gradient.x1 = a.x1 !== undefined ? coord( a.x1 ) : 0;
|
|
|
+ gradient.y1 = a.y1 !== undefined ? coord( a.y1 ) : 0;
|
|
|
+ gradient.x2 = a.x2 !== undefined ? coord( a.x2 ) : ( units === 'objectBoundingBox' ? 1 : 0 );
|
|
|
+ gradient.y2 = a.y2 !== undefined ? coord( a.y2 ) : 0;
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ const defCenter = units === 'objectBoundingBox' ? 0.5 : 0;
|
|
|
+ const defR = units === 'objectBoundingBox' ? 0.5 : 0;
|
|
|
+ gradient.cx = a.cx !== undefined ? coord( a.cx ) : defCenter;
|
|
|
+ gradient.cy = a.cy !== undefined ? coord( a.cy ) : defCenter;
|
|
|
+ gradient.r = a.r !== undefined ? coord( a.r ) : defR;
|
|
|
+ gradient.fx = a.fx !== undefined ? coord( a.fx ) : gradient.cx;
|
|
|
+ gradient.fy = a.fy !== undefined ? coord( a.fy ) : gradient.cy;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ gradients[ id ] = gradient;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
function parseStyle( node, style ) {
|
|
|
|
|
|
style = Object.assign( {}, style ); // clone style
|
|
|
@@ -1081,8 +1231,6 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
if ( adjustFunction === undefined ) adjustFunction = function copy( v ) {
|
|
|
|
|
|
- if ( v.startsWith( 'url' ) ) console.warn( 'SVGLoader: url access in attributes is not implemented.' );
|
|
|
-
|
|
|
return v;
|
|
|
|
|
|
};
|
|
|
@@ -1504,7 +1652,6 @@ class SVGLoader extends Loader {
|
|
|
function parseNodeTransform( node ) {
|
|
|
|
|
|
const transform = new Matrix3();
|
|
|
- const currentTransform = tempTransform0;
|
|
|
|
|
|
if ( node.nodeName === 'use' && ( node.hasAttribute( 'x' ) || node.hasAttribute( 'y' ) ) ) {
|
|
|
|
|
|
@@ -1517,138 +1664,148 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
if ( node.hasAttribute( 'transform' ) ) {
|
|
|
|
|
|
- const transformsTexts = node.getAttribute( 'transform' ).split( ')' );
|
|
|
+ parseTransformString( node.getAttribute( 'transform' ), transform );
|
|
|
|
|
|
- for ( let tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex -- ) {
|
|
|
+ }
|
|
|
|
|
|
- const transformText = transformsTexts[ tIndex ].trim();
|
|
|
+ return transform;
|
|
|
|
|
|
- if ( transformText === '' ) continue;
|
|
|
+ }
|
|
|
|
|
|
- const openParPos = transformText.indexOf( '(' );
|
|
|
- const closeParPos = transformText.length;
|
|
|
+ function parseTransformString( text, transform ) {
|
|
|
|
|
|
- if ( openParPos > 0 && openParPos < closeParPos ) {
|
|
|
+ const currentTransform = tempTransform0;
|
|
|
|
|
|
- const transformType = transformText.slice( 0, openParPos );
|
|
|
+ const transformsTexts = text.split( ')' );
|
|
|
|
|
|
- const array = parseFloats( transformText.slice( openParPos + 1 ) );
|
|
|
+ for ( let tIndex = transformsTexts.length - 1; tIndex >= 0; tIndex -- ) {
|
|
|
|
|
|
- currentTransform.identity();
|
|
|
+ const transformText = transformsTexts[ tIndex ].trim();
|
|
|
|
|
|
- switch ( transformType ) {
|
|
|
+ if ( transformText === '' ) continue;
|
|
|
|
|
|
- case 'translate':
|
|
|
+ const openParPos = transformText.indexOf( '(' );
|
|
|
+ const closeParPos = transformText.length;
|
|
|
|
|
|
- if ( array.length >= 1 ) {
|
|
|
+ if ( openParPos > 0 && openParPos < closeParPos ) {
|
|
|
|
|
|
- const tx = array[ 0 ];
|
|
|
- let ty = 0;
|
|
|
+ const transformType = transformText.slice( 0, openParPos );
|
|
|
|
|
|
- if ( array.length >= 2 ) {
|
|
|
+ const array = parseFloats( transformText.slice( openParPos + 1 ) );
|
|
|
|
|
|
- ty = array[ 1 ];
|
|
|
+ currentTransform.identity();
|
|
|
|
|
|
- }
|
|
|
+ switch ( transformType ) {
|
|
|
|
|
|
- currentTransform.translate( tx, ty );
|
|
|
+ case 'translate':
|
|
|
+
|
|
|
+ if ( array.length >= 1 ) {
|
|
|
+
|
|
|
+ const tx = array[ 0 ];
|
|
|
+ let ty = 0;
|
|
|
+
|
|
|
+ if ( array.length >= 2 ) {
|
|
|
+
|
|
|
+ ty = array[ 1 ];
|
|
|
|
|
|
}
|
|
|
|
|
|
- break;
|
|
|
+ currentTransform.translate( tx, ty );
|
|
|
|
|
|
- case 'rotate':
|
|
|
+ }
|
|
|
|
|
|
- if ( array.length >= 1 ) {
|
|
|
+ break;
|
|
|
|
|
|
- let angle = 0;
|
|
|
- let cx = 0;
|
|
|
- let cy = 0;
|
|
|
+ case 'rotate':
|
|
|
|
|
|
- // Angle
|
|
|
- angle = array[ 0 ] * Math.PI / 180;
|
|
|
+ if ( array.length >= 1 ) {
|
|
|
|
|
|
- if ( array.length >= 3 ) {
|
|
|
+ let angle = 0;
|
|
|
+ let cx = 0;
|
|
|
+ let cy = 0;
|
|
|
|
|
|
- // Center x, y
|
|
|
- cx = array[ 1 ];
|
|
|
- cy = array[ 2 ];
|
|
|
+ // Angle
|
|
|
+ angle = array[ 0 ] * Math.PI / 180;
|
|
|
|
|
|
- }
|
|
|
+ if ( array.length >= 3 ) {
|
|
|
|
|
|
- // Rotate around center (cx, cy)
|
|
|
- tempTransform1.makeTranslation( - cx, - cy );
|
|
|
- tempTransform2.makeRotation( angle );
|
|
|
- tempTransform3.multiplyMatrices( tempTransform2, tempTransform1 );
|
|
|
- tempTransform1.makeTranslation( cx, cy );
|
|
|
- currentTransform.multiplyMatrices( tempTransform1, tempTransform3 );
|
|
|
+ // Center x, y
|
|
|
+ cx = array[ 1 ];
|
|
|
+ cy = array[ 2 ];
|
|
|
|
|
|
}
|
|
|
|
|
|
- break;
|
|
|
+ // Rotate around center (cx, cy)
|
|
|
+ tempTransform1.makeTranslation( - cx, - cy );
|
|
|
+ tempTransform2.makeRotation( angle );
|
|
|
+ tempTransform3.multiplyMatrices( tempTransform2, tempTransform1 );
|
|
|
+ tempTransform1.makeTranslation( cx, cy );
|
|
|
+ currentTransform.multiplyMatrices( tempTransform1, tempTransform3 );
|
|
|
|
|
|
- case 'scale':
|
|
|
+ }
|
|
|
|
|
|
- if ( array.length >= 1 ) {
|
|
|
+ break;
|
|
|
|
|
|
- const scaleX = array[ 0 ];
|
|
|
- let scaleY = scaleX;
|
|
|
+ case 'scale':
|
|
|
|
|
|
- if ( array.length >= 2 ) {
|
|
|
+ if ( array.length >= 1 ) {
|
|
|
|
|
|
- scaleY = array[ 1 ];
|
|
|
+ const scaleX = array[ 0 ];
|
|
|
+ let scaleY = scaleX;
|
|
|
|
|
|
- }
|
|
|
+ if ( array.length >= 2 ) {
|
|
|
|
|
|
- currentTransform.scale( scaleX, scaleY );
|
|
|
+ scaleY = array[ 1 ];
|
|
|
|
|
|
}
|
|
|
|
|
|
- break;
|
|
|
+ currentTransform.scale( scaleX, scaleY );
|
|
|
|
|
|
- case 'skewX':
|
|
|
+ }
|
|
|
|
|
|
- if ( array.length === 1 ) {
|
|
|
+ break;
|
|
|
|
|
|
- currentTransform.set(
|
|
|
- 1, Math.tan( array[ 0 ] * Math.PI / 180 ), 0,
|
|
|
- 0, 1, 0,
|
|
|
- 0, 0, 1
|
|
|
- );
|
|
|
+ case 'skewX':
|
|
|
|
|
|
- }
|
|
|
+ if ( array.length === 1 ) {
|
|
|
|
|
|
- break;
|
|
|
+ currentTransform.set(
|
|
|
+ 1, Math.tan( array[ 0 ] * Math.PI / 180 ), 0,
|
|
|
+ 0, 1, 0,
|
|
|
+ 0, 0, 1
|
|
|
+ );
|
|
|
|
|
|
- case 'skewY':
|
|
|
+ }
|
|
|
|
|
|
- if ( array.length === 1 ) {
|
|
|
+ break;
|
|
|
|
|
|
- currentTransform.set(
|
|
|
- 1, 0, 0,
|
|
|
- Math.tan( array[ 0 ] * Math.PI / 180 ), 1, 0,
|
|
|
- 0, 0, 1
|
|
|
- );
|
|
|
+ case 'skewY':
|
|
|
|
|
|
- }
|
|
|
+ if ( array.length === 1 ) {
|
|
|
|
|
|
- break;
|
|
|
+ currentTransform.set(
|
|
|
+ 1, 0, 0,
|
|
|
+ Math.tan( array[ 0 ] * Math.PI / 180 ), 1, 0,
|
|
|
+ 0, 0, 1
|
|
|
+ );
|
|
|
|
|
|
- case 'matrix':
|
|
|
+ }
|
|
|
|
|
|
- if ( array.length === 6 ) {
|
|
|
+ break;
|
|
|
|
|
|
- currentTransform.set(
|
|
|
- array[ 0 ], array[ 2 ], array[ 4 ],
|
|
|
- array[ 1 ], array[ 3 ], array[ 5 ],
|
|
|
- 0, 0, 1
|
|
|
- );
|
|
|
+ case 'matrix':
|
|
|
|
|
|
- }
|
|
|
+ if ( array.length === 6 ) {
|
|
|
|
|
|
- break;
|
|
|
+ currentTransform.set(
|
|
|
+ array[ 0 ], array[ 2 ], array[ 4 ],
|
|
|
+ array[ 1 ], array[ 3 ], array[ 5 ],
|
|
|
+ 0, 0, 1
|
|
|
+ );
|
|
|
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -1972,6 +2129,7 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
const paths = [];
|
|
|
const stylesheets = {};
|
|
|
+ const gradients = {};
|
|
|
|
|
|
const transformStack = [];
|
|
|
|
|
|
@@ -1986,6 +2144,8 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
const xml = new DOMParser().parseFromString( text, 'image/svg+xml' ); // application/xml
|
|
|
|
|
|
+ parseGradients( xml );
|
|
|
+
|
|
|
parseNode( xml.documentElement, {
|
|
|
fill: '#000',
|
|
|
fillOpacity: 1,
|
|
|
@@ -1996,7 +2156,7 @@ class SVGLoader extends Loader {
|
|
|
strokeMiterLimit: 4
|
|
|
} );
|
|
|
|
|
|
- const data = { paths: paths, xml: xml.documentElement };
|
|
|
+ const data = { paths: paths, gradients: gradients, xml: xml.documentElement };
|
|
|
|
|
|
// console.log( paths );
|
|
|
return data;
|
|
|
@@ -2014,14 +2174,37 @@ class SVGLoader extends Loader {
|
|
|
const style = shapePath.userData.style;
|
|
|
if ( style.fill === undefined || style.fill === 'none' ) return null;
|
|
|
|
|
|
- return new MeshBasicMaterial( {
|
|
|
- color: shapePath.color,
|
|
|
+ const color = shapePath.color;
|
|
|
+ let texture = null;
|
|
|
+
|
|
|
+ const urlMatch = GRADIENT_URL_RE.exec( style.fill );
|
|
|
+
|
|
|
+ if ( urlMatch ) {
|
|
|
+
|
|
|
+ const gradient = shapePath.userData.gradients && shapePath.userData.gradients[ urlMatch[ 1 ] ];
|
|
|
+ texture = buildGradientTexture( gradient, shapePath );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const material = new MeshBasicMaterial( {
|
|
|
opacity: style.fillOpacity * ( style.opacity || 1 ),
|
|
|
transparent: true,
|
|
|
side: DoubleSide,
|
|
|
depthWrite: false,
|
|
|
} );
|
|
|
|
|
|
+ if ( texture !== null ) {
|
|
|
+
|
|
|
+ material.map = texture;
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ material.color = color;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return material;
|
|
|
+
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -2033,8 +2216,15 @@ class SVGLoader extends Loader {
|
|
|
static createStrokeMaterial( shapePath ) {
|
|
|
|
|
|
const style = shapePath.userData.style;
|
|
|
+
|
|
|
if ( style.stroke === undefined || style.stroke === 'none' ) return null;
|
|
|
|
|
|
+ if ( GRADIENT_URL_RE.test( style.stroke ) ) {
|
|
|
+
|
|
|
+ console.warn( 'THREE.SVGLoader: Gradient strokes are not supported.' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
return new MeshBasicMaterial( {
|
|
|
color: new Color().setStyle( style.stroke, COLOR_SPACE_SVG ),
|
|
|
opacity: style.strokeOpacity * ( style.opacity || 1 ),
|
|
|
@@ -3081,6 +3271,203 @@ class SVGLoader extends Loader {
|
|
|
|
|
|
}
|
|
|
|
|
|
+}
|
|
|
+
|
|
|
+const GRADIENT_URL_RE = /^\s*url\(\s*(?:["']\s*)?#([^)'"\s]+)(?:\s*["'])?\s*\)\s*$/;
|
|
|
+
|
|
|
+// Bakes a gradient into a CanvasTexture in its own local frame and configures
|
|
|
+// `texture.matrix` (with `matrixAutoUpdate = false`) so that shape-space UVs —
|
|
|
+// which, because transformPath bakes the world matrix into geometry vertex
|
|
|
+// positions, equal world xy — sample the correct gradient color. The caller
|
|
|
+// just sets `material.map = texture`; no bounding box, no geometry, no
|
|
|
+// per-vertex UV work required.
|
|
|
+function buildGradientTexture( gradient, shapePath, resolution = 256 ) {
|
|
|
+
|
|
|
+ if ( ! gradient || ! Array.isArray( gradient.stops ) || gradient.stops.length === 0 ) return null;
|
|
|
+
|
|
|
+ const worldTransform = shapePath.userData.transform;
|
|
|
+ const isBBoxUnits = gradient.gradientUnits === 'objectBoundingBox';
|
|
|
+
|
|
|
+ // For objectBoundingBox gradients we need the element's local bounding
|
|
|
+ // box. Path points are in world space (transformPath already applied the
|
|
|
+ // world transform), so invert that first.
|
|
|
+ let localBBox = null;
|
|
|
+
|
|
|
+ if ( isBBoxUnits ) {
|
|
|
+
|
|
|
+ localBBox = computeLocalBBox( shapePath, worldTransform );
|
|
|
+ if ( localBBox === null ) return null;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Resolves a gradient-space point to the geometry's (world) coordinate
|
|
|
+ // space: gradient coord → gradientTransform → target coord → (for
|
|
|
+ // objectBoundingBox: bbox → local) → worldTransform → world.
|
|
|
+ function resolvePoint( x, y, out ) {
|
|
|
+
|
|
|
+ out.set( x, y, 1 );
|
|
|
+ if ( gradient.gradientTransform ) out.applyMatrix3( gradient.gradientTransform );
|
|
|
+ if ( isBBoxUnits ) out.set(
|
|
|
+ localBBox.minX + out.x * localBBox.width,
|
|
|
+ localBBox.minY + out.y * localBBox.height,
|
|
|
+ 1,
|
|
|
+ );
|
|
|
+ if ( worldTransform ) out.applyMatrix3( worldTransform );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const canvas = document.createElement( 'canvas' );
|
|
|
+ let textureMatrix;
|
|
|
+
|
|
|
+ if ( gradient.type === 'linearGradient' ) {
|
|
|
+
|
|
|
+ // 1D bake along the gradient vector.
|
|
|
+ canvas.width = resolution;
|
|
|
+ canvas.height = 1;
|
|
|
+ const ctx = canvas.getContext( '2d' );
|
|
|
+
|
|
|
+ const grad = ctx.createLinearGradient( 0, 0, resolution, 0 );
|
|
|
+ addStops( grad, gradient.stops );
|
|
|
+ ctx.fillStyle = grad;
|
|
|
+ ctx.fillRect( 0, 0, resolution, 1 );
|
|
|
+
|
|
|
+ const p1 = new Vector3();
|
|
|
+ const p2 = new Vector3();
|
|
|
+ resolvePoint( gradient.x1, gradient.y1, p1 );
|
|
|
+ resolvePoint( gradient.x2, gradient.y2, p2 );
|
|
|
+
|
|
|
+ const dx = p2.x - p1.x;
|
|
|
+ const dy = p2.y - p1.y;
|
|
|
+ const len2 = dx * dx + dy * dy || 1e-20;
|
|
|
+ const a = dx / len2;
|
|
|
+ const b = dy / len2;
|
|
|
+ const c = - ( a * p1.x + b * p1.y );
|
|
|
+
|
|
|
+ // M * (vx, vy, 1) = (t, 0.5, 1)
|
|
|
+ textureMatrix = new Matrix3().set(
|
|
|
+ a, b, c,
|
|
|
+ 0, 0, 0.5,
|
|
|
+ 0, 0, 1,
|
|
|
+ );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ // Resolve cx/cy/fx/fy into local space and scale r per the SVG spec
|
|
|
+ // (objectBoundingBox scales lengths by sqrt((w² + h²) / 2)). The canvas
|
|
|
+ // only draws circular radial gradients, so any ellipticity induced by
|
|
|
+ // a non-uniform world transform is picked up later via the UV matrix.
|
|
|
+ let cx = gradient.cx, cy = gradient.cy;
|
|
|
+ let fx = gradient.fx, fy = gradient.fy;
|
|
|
+ let r = gradient.r;
|
|
|
+
|
|
|
+ if ( gradient.gradientTransform ) {
|
|
|
+
|
|
|
+ const tmp = new Vector3();
|
|
|
+ tmp.set( cx, cy, 1 ).applyMatrix3( gradient.gradientTransform );
|
|
|
+ cx = tmp.x; cy = tmp.y;
|
|
|
+ tmp.set( fx, fy, 1 ).applyMatrix3( gradient.gradientTransform );
|
|
|
+ fx = tmp.x; fy = tmp.y;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( isBBoxUnits ) {
|
|
|
+
|
|
|
+ cx = localBBox.minX + cx * localBBox.width;
|
|
|
+ cy = localBBox.minY + cy * localBBox.height;
|
|
|
+ fx = localBBox.minX + fx * localBBox.width;
|
|
|
+ fy = localBBox.minY + fy * localBBox.height;
|
|
|
+ r = r * Math.sqrt( ( localBBox.width * localBBox.width + localBBox.height * localBBox.height ) / 2 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( r <= 0 ) return null;
|
|
|
+
|
|
|
+ // 2D bake in the gradient's local frame, covering [cx-r, cx+r]².
|
|
|
+ canvas.width = resolution;
|
|
|
+ canvas.height = resolution;
|
|
|
+ const ctx = canvas.getContext( '2d' );
|
|
|
+
|
|
|
+ const localMinX = cx - r;
|
|
|
+ const localMinY = cy - r;
|
|
|
+ const localSpan = 2 * r;
|
|
|
+ const scale = resolution / localSpan;
|
|
|
+
|
|
|
+ // Canvas pixel = (local - localMin) * scale.
|
|
|
+ ctx.setTransform( scale, 0, 0, scale, - localMinX * scale, - localMinY * scale );
|
|
|
+
|
|
|
+ const grad = ctx.createRadialGradient( fx, fy, 0, cx, cy, r );
|
|
|
+ addStops( grad, gradient.stops );
|
|
|
+ ctx.fillStyle = grad;
|
|
|
+ ctx.fillRect( localMinX, localMinY, localSpan, localSpan );
|
|
|
+
|
|
|
+ // UV matrix: world → local (via worldTransform⁻¹) → normalized canvas UV.
|
|
|
+ const inv = worldTransform ? worldTransform.clone().invert() : new Matrix3();
|
|
|
+ const norm = new Matrix3().set(
|
|
|
+ 1 / localSpan, 0, - localMinX / localSpan,
|
|
|
+ 0, 1 / localSpan, - localMinY / localSpan,
|
|
|
+ 0, 0, 1,
|
|
|
+ );
|
|
|
+ textureMatrix = norm.multiply( inv );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const texture = new CanvasTexture( canvas );
|
|
|
+ texture.colorSpace = COLOR_SPACE_SVG;
|
|
|
+ texture.flipY = false;
|
|
|
+ texture.matrixAutoUpdate = false;
|
|
|
+ texture.matrix = textureMatrix;
|
|
|
+
|
|
|
+ const wrap = gradient.spreadMethod === 'reflect' ? MirroredRepeatWrapping
|
|
|
+ : gradient.spreadMethod === 'repeat' ? RepeatWrapping
|
|
|
+ : ClampToEdgeWrapping;
|
|
|
+ texture.wrapS = wrap;
|
|
|
+ texture.wrapT = wrap;
|
|
|
+
|
|
|
+ return texture;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function computeLocalBBox( shapePath, worldTransform ) {
|
|
|
+
|
|
|
+ const inv = worldTransform ? worldTransform.clone().invert() : null;
|
|
|
+ const tmp = new Vector2();
|
|
|
+ const box = new Box2();
|
|
|
+
|
|
|
+ for ( const subPath of shapePath.subPaths ) {
|
|
|
+
|
|
|
+ for ( const p of subPath.getPoints() ) {
|
|
|
+
|
|
|
+ tmp.copy( p );
|
|
|
+ if ( inv ) tmp.applyMatrix3( inv );
|
|
|
+ box.expandByPoint( tmp );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( box.isEmpty() ) return null;
|
|
|
+
|
|
|
+ return { minX: box.min.x, minY: box.min.y, width: box.max.x - box.min.x, height: box.max.y - box.min.y };
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function addStops( canvasGradient, stops ) {
|
|
|
+
|
|
|
+ const tmpColor = new Color();
|
|
|
+ for ( const stop of stops ) {
|
|
|
+
|
|
|
+ let css = stop.color;
|
|
|
+ if ( stop.opacity < 1 ) {
|
|
|
+
|
|
|
+ tmpColor.setStyle( stop.color, COLOR_SPACE_SVG );
|
|
|
+ const m = /rgb\(([^)]+)\)/.exec( tmpColor.getStyle( COLOR_SPACE_SVG ) );
|
|
|
+ if ( m ) css = `rgba(${m[ 1 ]},${stop.opacity})`;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ canvasGradient.addColorStop( Math.max( 0, Math.min( 1, stop.offset ) ), css );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
}
|
|
|
|