/
githubmirror
/
pixijs
Обзор
Документация
Войти
/
githubmirror
/
pixijs
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
src/scene/graphics/shared/fill/FillGradient.ts
609 строк
19 KB
Zyie
feat: add textureSpace support to FillPattern (#12037)
06 май 2026, 15:09
Не верифицирован
06 май 2026, 15:09
69bffc1
Код
Авторство
О чём код?
import { Color } from '../../../../color/Color'; import { DOMAdapter } from '../../../../environment/adapter'; import { Matrix } from '../../../../maths/matrix/Matrix'; import { type WRAP_MODE } from '../../../../rendering/renderers/shared/texture/const'; import { ImageSource } from '../../../../rendering/renderers/shared/texture/sources/ImageSource'; import { Texture } from '../../../../rendering/renderers/shared/texture/Texture'; import { uid } from '../../../../utils/data/uid'; import { deprecation } from '../../../../utils/logging/deprecation'; import { definedProps } from '../../../container/utils/definedProps'; import type { ColorSource } from '../../../../color/Color'; import type { PointData } from '../../../../maths/point/PointData'; import type { CanvasAndContext } from '../../../../rendering/renderers/shared/texture/CanvasPool'; import type { TextureSpace } from '../FillTypes'; /** * Defines the type of gradient to create. * * It can be: * - 'linear': A linear gradient that transitions colors along a straight line. * - 'radial': A radial gradient that transitions colors in a circular pattern from an inner circle to an outer circle. * @category scene * @standard */ export type GradientType = 'linear' | 'radial'; /** * Represents the style options for a linear gradient fill. * @category scene * @standard */ export interface BaseGradientOptions { /** The type of gradient */ type?: GradientType; /** Array of colors stops to use in the gradient */ colorStops?: { offset: number, color: ColorSource }[]; /** Whether coordinates are 'global' or 'local' */ textureSpace?: TextureSpace; /** * The size of the texture to use for the gradient - this is for advanced usage. * The texture size does not need to match the size of the object being drawn. * Due to GPU interpolation, gradient textures can be relatively small! * Consider using a larger texture size if your gradient has a lot of very tight color steps */ textureSize?: number; /** * The wrap mode of the gradient. * This can be 'clamp-to-edge' or 'repeat'. * @default 'clamp-to-edge' */ wrapMode?: WRAP_MODE } /** * Options specific to linear gradients. * A linear gradient creates a smooth transition between colors along a straight line defined by start and end points. * @category scene * @standard */ export interface LinearGradientOptions extends BaseGradientOptions { /** The type of gradient. Must be 'linear' for linear gradients. */ type?: 'linear'; /** * The start point of the gradient. * This point defines where the gradient begins. * It is represented as a PointData object containing x and y coordinates. * The coordinates are in local space by default (0-1), but can be in global space if specified. */ start?: PointData; /** * The end point of the gradient. * This point defines where the gradient ends. * It is represented as a PointData object containing x and y coordinates. * The coordinates are in local space by default (0-1), but can be in global space if specified. */ end?: PointData; } /** * Options specific to radial gradients. * A radial gradient creates a smooth transition between colors that radiates outward in a circular pattern. * The gradient is defined by inner and outer circles, each with their own radius. * @category scene * @standard */ export interface RadialGradientOptions extends BaseGradientOptions { /** The type of gradient. Must be 'radial' for radial gradients. */ type?: 'radial'; /** The center point of the inner circle where the gradient begins. In local coordinates by default (0-1). */ center?: PointData; /** The radius of the inner circle where the gradient begins. */ innerRadius?: number; /** The center point of the outer circle where the gradient ends. In local coordinates by default (0-1). */ outerCenter?: PointData; /** The radius of the outer circle where the gradient ends. */ outerRadius?: number; /** * The y scale of the gradient, use this to make the gradient elliptical. * NOTE: Only applied to radial gradients used with Graphics. */ scale?: number; /** * The rotation of the gradient in radians, useful for making the gradient elliptical. * NOTE: Only applied to radial gradients used with Graphics. */ rotation?: number; } /** * Options for creating a gradient fill. * @category scene * @standard */ export type GradientOptions = LinearGradientOptions | RadialGradientOptions; /** * If no color stops are provided, we use a default gradient of white to black - this is to avoid a blank gradient if a dev * forgets to set them. */ const emptyColorStops: { offset: number, color: string }[] = [{ offset: 0, color: 'white' }, { offset: 1, color: 'black' }]; /** * Class representing a gradient fill that can be used to fill shapes and text. * Supports both linear and radial gradients with multiple color stops. * * For linear gradients, color stops define colors and positions (0 to 1) along a line from start point (x0,y0) * to end point (x1,y1). * * For radial gradients, color stops define colors between two circles - an inner circle centered at (x0,y0) with radius r0, * and an outer circle centered at (x1,y1) with radius r1. * @example * ```ts * // Create a vertical linear gradient from red to blue * const linearGradient = new FillGradient({ * type: 'linear', * start: { x: 0, y: 0 }, // Start at top * end: { x: 0, y: 1 }, // End at bottom * colorStops: [ * { offset: 0, color: 'red' }, // Red at start * { offset: 1, color: 'blue' } // Blue at end * ], * // Use normalized coordinate system where (0,0) is the top-left and (1,1) is the bottom-right of the shape * textureSpace: 'local' * }); * * // Create a radial gradient from yellow center to green edge * const radialGradient = new FillGradient({ * type: 'radial', * center: { x: 0.5, y: 0.5 }, * innerRadius: 0, * outerCenter: { x: 0.5, y: 0.5 }, * outerRadius: 0.5, * colorStops: [ * { offset: 0, color: 'yellow' }, // Center color * { offset: 1, color: 'green' } // Edge color * ], * // Use normalized coordinate system where (0,0) is the top-left and (1,1) is the bottom-right of the shape * textureSpace: 'local' * }); * * // Create a rainbow linear gradient in global coordinates * const globalGradient = new FillGradient({ * type: 'linear', * start: { x: 0, y: 0 }, * end: { x: 100, y: 0 }, * colorStops: [ * { offset: 0, color: 0xff0000 }, // Red * { offset: 0.33, color: 0x00ff00 }, // Green * { offset: 0.66, color: 0x0000ff }, // Blue * { offset: 1, color: 0xff00ff } // Purple * ], * textureSpace: 'global' // Use world coordinates * }); * * // Create an offset radial gradient * const offsetRadial = new FillGradient({ * type: 'radial', * center: { x: 0.3, y: 0.3 }, * innerRadius: 0.1, * outerCenter: { x: 0.5, y: 0.5 }, * outerRadius: 0.5, * colorStops: [ * { offset: 0, color: 'white' }, * { offset: 1, color: 'black' } * ], * // Use normalized coordinate system where (0,0) is the top-left and (1,1) is the bottom-right of the shape * textureSpace: 'local' * }); * ``` * * Internally this creates a texture of the gradient then applies a * transform to it to give it the correct size and angle. * * This means that it's important to destroy a gradient when it is no longer needed * to avoid memory leaks. * * If you want to animate a gradient then it's best to modify and update an existing one * rather than creating a whole new one each time. That or use a custom shader. * @category scene * @standard */ export class FillGradient implements CanvasGradient { /** Default options for creating a gradient fill */ public static readonly defaultLinearOptions: LinearGradientOptions = { start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, colorStops: [], textureSpace: 'local', type: 'linear', textureSize: 256, wrapMode: 'clamp-to-edge' }; /** Default options for creating a radial gradient fill */ public static readonly defaultRadialOptions: RadialGradientOptions = { center: { x: 0.5, y: 0.5 }, innerRadius: 0, outerRadius: 0.5, colorStops: [], scale: 1, rotation: 0, textureSpace: 'local', type: 'radial', textureSize: 256, wrapMode: 'clamp-to-edge' }; /** * Unique identifier for this gradient instance * @internal */ public readonly uid: number = uid('fillGradient'); /** * Internal tick counter to track changes in the gradient. * This is used to invalidate the gradient when the texture changes. * @internal */ public _tick: number = 0; /** Type of gradient - currently only supports 'linear' */ public readonly type: GradientType = 'linear'; /** Internal texture used to render the gradient */ public texture: Texture; /** Transform matrix for positioning the gradient */ public transform: Matrix; /** Array of color stops defining the gradient */ public colorStops: Array<{ offset: number, color: string }> = []; /** Whether gradient coordinates are in local or global space */ public textureSpace: TextureSpace; private readonly _textureSize: number; /** The start point of the linear gradient */ public start: PointData; /** The end point of the linear gradient */ public end: PointData; /** The wrap mode of the gradient texture */ private readonly _wrapMode: WRAP_MODE; /** The center point of the inner circle of the radial gradient */ public center: PointData; /** The center point of the outer circle of the radial gradient */ public outerCenter: PointData; /** The radius of the inner circle of the radial gradient */ public innerRadius: number; /** The radius of the outer circle of the radial gradient */ public outerRadius: number; /** The scale of the radial gradient */ public scale: number; /** The rotation of the radial gradient */ public rotation: number; /** * Creates a new gradient fill. The constructor behavior changes based on the gradient type. * @param {GradientOptions} options - The options for the gradient * @see {@link LinearGradientOptions} * @see {@link RadialGradientOptions} */ constructor(options: GradientOptions); /** * Deprecated: Use the options object instead. * @deprecated since 8.5.2 * @ignore */ constructor( x0?: number, y0?: number, x1?: number, y1?: number, textureSpace?: TextureSpace, textureSize?: number ); constructor(...args: [GradientOptions] | [number?, number?, number?, number?, TextureSpace?, number?]) { let options = ensureGradientOptions(args); const defaults = options.type === 'radial' ? FillGradient.defaultRadialOptions : FillGradient.defaultLinearOptions; options = { ...defaults, ...definedProps(options) }; this._textureSize = options.textureSize; this._wrapMode = options.wrapMode; if (options.type === 'radial') { this.center = options.center; this.outerCenter = options.outerCenter ?? this.center; this.innerRadius = options.innerRadius; this.outerRadius = options.outerRadius; this.scale = options.scale; this.rotation = options.rotation; } else { this.start = options.start; this.end = options.end; } this.textureSpace = options.textureSpace; this.type = options.type; options.colorStops.forEach((stop) => { this.addColorStop(stop.offset, stop.color); }); } /** * Adds a color stop to the gradient * @param offset - Position of the stop (0-1) * @param color - Color of the stop * @returns This gradient instance for chaining */ public addColorStop(offset: number, color: ColorSource): this { this.colorStops.push({ offset, color: Color.shared.setValue(color).toHexa() }); return this; } /** * Builds the internal texture and transform for the gradient. * Called automatically when the gradient is first used. * @internal */ public buildLinearGradient(): void { if (this.texture) return; let { x: x0, y: y0 } = this.start; let { x: x1, y: y1 } = this.end; let dx = x1 - x0; let dy = y1 - y0; // Determine flip based on original dx/dy and swap coordinates if necessary const flip = dx < 0 || dy < 0; if (this._wrapMode === 'clamp-to-edge') { if (dx < 0) { const temp = x0; x0 = x1; x1 = temp; dx *= -1; } if (dy < 0) { const temp = y0; y0 = y1; y1 = temp; dy *= -1; } } const colorStops = this.colorStops.length ? this.colorStops : emptyColorStops; const defaultSize = this._textureSize; const { canvas, context } = getCanvas(defaultSize, 1); const gradient = !flip ? context.createLinearGradient(0, 0, this._textureSize, 0) : context.createLinearGradient(this._textureSize, 0, 0, 0); addColorStops(gradient, colorStops); context.fillStyle = gradient; context.fillRect(0, 0, defaultSize, 1); this.texture = new Texture({ source: new ImageSource({ resource: canvas, addressMode: this._wrapMode, }), }); // generate some UVS based on the gradient direction sent const dist = Math.sqrt((dx * dx) + (dy * dy)); const angle = Math.atan2(dy, dx); // little offset to stop the uvs from flowing over the edge.. // this matrix is inverted when used in the graphics // add a tiny off set to prevent uv bleeding.. const m = new Matrix(); m.scale((dist / defaultSize), 1); m.rotate(angle); m.translate(x0, y0); if (this.textureSpace === 'local') { m.scale(defaultSize, defaultSize); } this.transform = m; } /** * Builds the internal texture and transform for the gradient. * Called automatically when the gradient is first used. * @internal */ public buildGradient(): void { if (!this.texture) this._tick++; if (this.type === 'linear') { this.buildLinearGradient(); } else { this.buildRadialGradient(); } } /** * Builds the internal texture and transform for the radial gradient. * Called automatically when the gradient is first used. * @internal */ public buildRadialGradient(): void { if (this.texture) return; const colorStops = this.colorStops.length ? this.colorStops : emptyColorStops; const defaultSize = this._textureSize; const { canvas, context } = getCanvas(defaultSize, defaultSize); const { x: x0, y: y0 } = this.center; const { x: x1, y: y1 } = this.outerCenter; const r0 = this.innerRadius; const r1 = this.outerRadius; const ox = x1 - r1; const oy = y1 - r1; const scale = defaultSize / (r1 * 2); const cx = (x0 - ox) * scale; const cy = (y0 - oy) * scale; const gradient = context.createRadialGradient( cx, cy, r0 * scale, (x1 - ox) * scale, (y1 - oy) * scale, r1 * scale ); addColorStops(gradient, colorStops); context.fillStyle = colorStops[colorStops.length - 1].color; context.fillRect(0, 0, defaultSize, defaultSize); context.fillStyle = gradient; // First translate to center context.translate(cx, cy); // Then apply rotation context.rotate(this.rotation); // Then scale2 context.scale(1, this.scale); // Finally translate back, taking scale into account context.translate(-cx, -cy); context.fillRect(0, 0, defaultSize, defaultSize); this.texture = new Texture({ source: new ImageSource({ resource: canvas, addressMode: this._wrapMode, }), }); // this matrix is inverted when used in the graphics const m = new Matrix(); if (this.textureSpace === 'local') { m.scale(2 * r1, 2 * r1); } else { m.scale(1 / scale, 1 / scale); } m.translate(ox, oy); this.transform = m; } /** Destroys the gradient, releasing resources. This will also destroy the internal texture. */ public destroy(): void { this.texture?.destroy(true); this.texture = null; this.transform = null; this.colorStops = []; this.start = null; this.end = null; this.center = null; this.outerCenter = null; } /** * Returns a unique key for this gradient instance. * This key is used for caching and texture management. * @returns {string} Unique key for the gradient */ public get styleKey(): string { return `fill-gradient-${this.uid}-${this._tick}`; } } function addColorStops(gradient: CanvasGradient, colorStops: { offset: number, color: string }[]): void { for (let i = 0; i < colorStops.length; i++) { const stop = colorStops[i]; gradient.addColorStop(stop.offset, stop.color); } } function getCanvas(width: number, height: number): CanvasAndContext { const canvas = DOMAdapter.get().createCanvas(width, height); const context = canvas.getContext('2d'); return { canvas, context }; } /** * Helper function to ensure consistent handling of gradient options. * This function handles both the new options object format and the deprecated parameter format. * @example * // New recommended way: * const options = ensureGradientOptions({ * start: { x: 0, y: 0 }, * end: { x: 100, y: 100 }, * textureSpace: 'local' * }); * * // Deprecated way (will show warning in debug): * const options = ensureGradientOptions([0, 0, 100, 100, 'local']); * @param args - Arguments passed to gradient constructor * @returns Normalized gradient options object * @internal */ function ensureGradientOptions( args: any[], ): GradientOptions { let options = (args[0] ?? {}) as GradientOptions; // @deprecated if (typeof options === 'number' || args[1]) { // #if _DEBUG deprecation('8.5.2', `use options object instead`); // #endif options = { type: 'linear', start: { x: args[0], y: args[1] }, end: { x: args[2], y: args[3] }, textureSpace: args[4] as 'global' | 'local', textureSize: args[5] ?? FillGradient.defaultLinearOptions.textureSize }; } return options; }