Skip to content

Commit

Permalink
Add gradient support (#71)
Browse files Browse the repository at this point in the history
The model now supports having SVG gradients applied to individual
chunks. A new `gradients` property has been added to the JSON model
that contains a map of gradient IDs to definitions. A gradient is
applied to a chunk by setting the optional chunk property `gradient` to
a gradient ID.

Both linear and radial gradients are supported, along with most SVG
attributes. Any attribute with a limited set of options is validated to
ensure it is set correctly, but the attributes that take lengths or
colors simply ensure the value is a string (validating them property
would be challenging). Validation has been added to throw an error if
any unsupported attributes are present.

The "gradient" demo has been updated to use this new gradient support.
Now that example sets the gradient on each chunk rather than applying
an overlay and masking it to match the model's shape. This greatly
simplifies the example.

Also the example-specific model files have been moved into the
`example` directory. A new model was needed for the updated gradient
example, and this seemed like a better place for it to go, so that the
top-level of the repository doesn't get too cluttered.
  • Loading branch information
Gudahtt authored Oct 13, 2021
1 parent 6e9801b commit 0ca4a5a
Show file tree
Hide file tree
Showing 11 changed files with 2,538 additions and 256 deletions.
317 changes: 304 additions & 13 deletions docs/beta/bundle.js

Large diffs are not rendered by default.

307 changes: 297 additions & 10 deletions docs/distort/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -823,10 +823,86 @@ module.exports = {
createFaceUpdater,
createNode,
setAttribute,
setGradientDefinitions,
svgElementToSvgImageContent,
Polygon,
};

/**
* A distance measurement used for SVG attributes. A length is specified as a number followed by a
* unit identifier.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#length} for further
* information.
*
* @typedef {`${number}${'em' | 'ex' | 'px' | 'in' | 'cm' | 'mm' | 'pt' | 'pc' | '%'}`} SvgLength
*/

/**
* A definition for a `<stop>` SVG element, which defines a color and the position for that color
* on a gradient. This element is always a child of either a `<linearGradient>` or
* `<radialGradient>` element.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/stop} for more information
* about the `<stop>` element.
*
* @typedef {object} StopDefinition
* @property {number | `${number}%`} [offset] - The location of the gradient stop along the
* gradient vector.
* @property {string} [stop-color] - The color of the gradient stop. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/stop}.
* @property {number} [stop-opacity] - The opacity of the gradient stop. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stop-opacity}.
*/

/**
* A definition for a `<linearGradient>` SVG element. This definition includes all supported
* `<linearGradient>` attributes, and it includes a `stops` property which is an array of
* definitions for each `<stop>` child node.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient} for more
* information about the `<linearGradient>` element.
*
* @typedef {object} LinearGradientDefinition
* @property {string} [gradientTransform] - A transform from the gradient coordinate system to the
* target coordinate system. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/gradientTransform}.
* @property {'userSpaceOnUse' | 'objectBoundingBox'} [gradientUnits] - The coordinate system used.
* for the coordinate attributes. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/gradientUnits}.
* @property {'pad' | 'reflect' | 'repeat'} [spreadMethod] - The method used to fill a shape beyond
* the defined edges of a gradient. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/spreadMethod}.
* @property {StopDefinition[]} [stops] - The colors of the gradient, and the position of each
* color along the gradient vector.
* @property {'linear'} type - The type of the gradient.
* @property {SvgLength} [x1] - The x coordinate of the starting point of the vector gradient.
* @property {SvgLength} [x2] - The x coordinate of the ending point of the vector gradient.
* @property {SvgLength} [y1] - The y coordinate of the starting point of the vector gradient.
* @property {SvgLength} [y2] - The y coordinate of the ending point of the vector gradient.
*/

/**
* A definition for a `<radialGradient>` SVG element. This definition includes all supported
* `<radialGradient>` attributes, and it includes a `stops` property which is an array of
* definitions for each `<stop>` child node.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/radialGradient} for more
* information about the `<radialGradient>` element.
*
* @typedef {object} RadialGradientDefinition
* @property {SvgLength} [cx] - The x coordinate of the end circle of the radial gradiant.
* @property {SvgLength} [cy] - The y coordinate of the end circle of the radial gradient.
* @property {SvgLength} [fr] - The radius of the start circle of the radial gradient.
* @property {SvgLength} [fx] - The x coordinate of the start circle of the radial gradient.
* @property {SvgLength} [fy] - The y coordinate of the start circle of the radial gradient.
* @property {string} [gradientTransform] - A transform from the gradient coordinate system to the
* target coordinate system. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/gradientTransform}.
* @property {'userSpaceOnUse' | 'objectBoundingBox'} [gradientUnits] - The coordinate system used
* for the coordinate attributes. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/gradientUnits}.
* @property {SvgLength} [r] - The radius of the end circle of the radial gradient.
* @property {'pad' | 'reflect' | 'repeat'} [spreadMethod] - The method used to fill a shape beyond
* the defined edges of a gradient. See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/spreadMethod}.
* @property {StopDefinition[]} [stops] - The colors of the gradient, and the position of each
* color along the gradient vector.
* @property {'radial'} type - The type of the gradient.
*/

function createLogoViewer(
container,
renderScene,
Expand Down Expand Up @@ -1014,10 +1090,13 @@ function positionsFromModel(positions, modelJson) {

function createPolygonsFromModelJson(modelJson, createSvgPolygon) {
const polygons = [];
const polygonsByChunk = modelJson.chunks.map((chunk) => {
const polygonsByChunk = modelJson.chunks.map((chunk, index) => {
const { faces } = chunk;
return faces.map((face) => {
const svgPolygon = createSvgPolygon(chunk);
const svgPolygon = createSvgPolygon(chunk, {
gradients: modelJson.gradients,
index,
});
const polygon = new Polygon(svgPolygon, face);
polygons.push(polygon);
return polygon;
Expand All @@ -1026,11 +1105,42 @@ function createPolygonsFromModelJson(modelJson, createSvgPolygon) {
return { polygons, polygonsByChunk };
}

function createStandardModelPolygon(chunk) {
const color = `rgb(${chunk.color})`;
/**
* Create an SVG `<polygon> element.
*
* This polygon is assigned the correct `fill` and `stroke` attributes, according to the chunk
* definition provided. But the `points` attribute is always set to a dummy value, as it gets reset
* later to the correct position during each render loop.
*
* @param {object} chunk - The definition for the chunk of the model this polygon is a part of.
* This includes the color or gradient to apply to the polygon.
* @param {object} options - Polygon options.
* @param {(LinearGradientDefinition | RadialGradientDefinition)[]} [options.gradients] - The set of
* all gradient definitions used in this model.
* @param options.index - The index for the chunk this polygon is found in.
* @returns {Element} The `<polygon>` SVG element.
*/
function createStandardModelPolygon(chunk, { gradients = {}, index }) {
const svgPolygon = createNode('polygon');
setAttribute(svgPolygon, 'fill', color);
setAttribute(svgPolygon, 'stroke', color);

if (chunk.gradient && chunk.color) {
throw new Error(
`Both gradient and color for chunk '${index}'. These options are mutually exclusive.`,
);
} else if (chunk.gradient) {
const gradientId = chunk.gradient;
if (!gradients[gradientId]) {
throw new Error(`Gradient ID not found: '${gradientId}'`);
}

setAttribute(svgPolygon, 'fill', `url('#${gradientId}')`);
setAttribute(svgPolygon, 'stroke', `url('#${gradientId}')`);
} else {
const fill = `rgb(${chunk.color})`;
setAttribute(svgPolygon, 'fill', fill);
setAttribute(svgPolygon, 'stroke', fill);
}

setAttribute(svgPolygon, 'points', '0,0, 10,0, 0,10');
return svgPolygon;
}
Expand Down Expand Up @@ -1176,10 +1286,10 @@ function createFaceUpdater(container, polygons, transformed) {
toDraw.push(poly);
}
toDraw.sort(compareZ);
container.innerHTML = '';
for (i = 0; i < toDraw.length; ++i) {
container.appendChild(toDraw[i].svg);
}

const newPolygons = toDraw.map((poly) => poly.svg);
const defs = container.getElementsByTagName('defs');
container.replaceChildren(...defs, ...newPolygons);
};
}

Expand Down Expand Up @@ -1223,4 +1333,181 @@ function Polygon(svg, indices) {
this.zIndex = 0;
}

/**
* Parse gradient definitions and construct them in the DOM.
*
* Both `<linearGradient>` and `<radialGradient>` are supported. All gradients get added to a
* `<defs>` element that is added as a direct child of the container element.
*
* @param {Element} container - The `<svg>` HTML element that the definitions should be added to.
* @param {(LinearGradientDefinition | RadialGradientDefinition)[]} [gradients] - The gradient definitions.
*/
function setGradientDefinitions(container, gradients) {
if (!gradients || Object.keys(gradients).length === 0) {
return;
}

const defsContainer = createNode('defs');

const linearCoordinateAttributes = ['x1', 'x2', 'y1', 'y2'];
const radialCoordinateAttributes = ['cx', 'cy', 'fr', 'fx', 'fy', 'r'];
const commonAttributes = [
'gradientTransform',
'gradientUnits',
'spreadMethod',
'stops',
'type',
];
const allLinearAttributes = [
...linearCoordinateAttributes,
...commonAttributes,
];
const allRadialAttributes = [
...radialCoordinateAttributes,
...commonAttributes,
];

for (const [gradientId, gradientDefinition] of Object.entries(gradients)) {
let gradient;
if (gradientDefinition.type === 'linear') {
gradient = createNode('linearGradient');

const unsupportedLinearAttribute = Object.keys(gradientDefinition).find(
(attribute) => !allLinearAttributes.includes(attribute),
);
if (unsupportedLinearAttribute) {
throw new Error(
`Unsupported linear gradient attribute: '${unsupportedLinearAttribute}'`,
);
} else if (
linearCoordinateAttributes.some(
(attributeName) => gradientDefinition[attributeName] !== undefined,
)
) {
const missingAttributes = linearCoordinateAttributes.filter(
(attributeName) => gradientDefinition[attributeName] === undefined,
);
if (missingAttributes.length > 0) {
throw new Error(
`Missing coordinate attributes: '${missingAttributes.join(', ')}'`,
);
}

for (const attribute of linearCoordinateAttributes) {
if (typeof gradientDefinition[attribute] !== 'string') {
throw new Error(
`Type of '${attribute}' option expected to be 'string'. Instead received type '${typeof gradientDefinition[
attribute
]}'`,
);
}
setAttribute(gradient, attribute, gradientDefinition[attribute]);
}
}
} else if (gradientDefinition.type === 'radial') {
gradient = createNode('radialGradient');

const presentCoordinateAttributes = radialCoordinateAttributes.filter(
(attributeName) => gradientDefinition[attributeName] !== undefined,
);
const unsupportedRadialAttribute = Object.keys(gradientDefinition).find(
(attribute) => !allRadialAttributes.includes(attribute),
);
if (unsupportedRadialAttribute) {
throw new Error(
`Unsupported radial gradient attribute: '${unsupportedRadialAttribute}'`,
);
} else if (presentCoordinateAttributes.length > 0) {
for (const attribute of presentCoordinateAttributes) {
if (typeof gradientDefinition[attribute] !== 'string') {
throw new Error(
`Type of '${attribute}' option expected to be 'string'. Instead received type '${typeof gradientDefinition[
attribute
]}'`,
);
}
setAttribute(gradient, attribute, gradientDefinition[attribute]);
}
}
} else {
throw new Error(
`Unsupported gradient type: '${gradientDefinition.type}'`,
);
}

// Set common attributes
setAttribute(gradient, 'id', gradientId);
if (gradientDefinition.gradientUnits !== undefined) {
if (
!['userSpaceOnUse', 'objectBoundingBox'].includes(
gradientDefinition.gradientUnits,
)
) {
throw new Error(
`Unrecognized value for 'gradientUnits' attribute: '${gradientDefinition.gradientUnits}'`,
);
}
setAttribute(gradient, 'gradientUnits', gradientDefinition.gradientUnits);
}

if (gradientDefinition.gradientTransform !== undefined) {
if (typeof gradientDefinition.gradientTransform !== 'string') {
throw new Error(
`Type of 'gradientTransform' option expected to be 'string'. Instead received type '${typeof gradientDefinition.gradientTransform}'`,
);
}

setAttribute(
gradient,
'gradientTransform',
gradientDefinition.gradientTransform,
);
}

if (gradientDefinition.spreadMethod !== undefined) {
if (
!['pad', 'reflect', 'repeat'].includes(gradientDefinition.spreadMethod)
) {
throw new Error(
`Unrecognized value for 'spreadMethod' attribute: '${gradientDefinition.spreadMethod}'`,
);
}
setAttribute(gradient, 'spreadMethod', gradientDefinition.spreadMethod);
}

if (gradientDefinition.stops !== undefined) {
if (!Array.isArray(gradientDefinition.stops)) {
throw new Error(`The 'stop' attribute must be an array`);
}

for (const stopDefinition of gradientDefinition.stops) {
if (typeof stopDefinition !== 'object') {
throw new Error(
`Each entry in the 'stop' attribute must be an object. Instead received type '${typeof stopDefinition}'`,
);
}
const stop = createNode('stop');

if (stopDefinition.offset !== undefined) {
setAttribute(stop, 'offset', stopDefinition.offset);
}

if (stopDefinition['stop-color'] !== undefined) {
setAttribute(stop, 'stop-color', stopDefinition['stop-color']);
}

if (stopDefinition['stop-opacity'] !== undefined) {
setAttribute(stop, 'stop-opacity', stopDefinition['stop-opacity']);
}

gradient.appendChild(stop);
}
}

defsContainer.appendChild(gradient);
}

container.appendChild(defsContainer);
}

},{"gl-mat4/invert":4,"gl-mat4/lookAt":5,"gl-mat4/multiply":6,"gl-mat4/perspective":7,"gl-mat4/rotate":8,"gl-vec3/transformMat4":9}]},{},[1]);
Loading

0 comments on commit 0ca4a5a

Please sign in to comment.