Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gradient support #71

Merged
merged 5 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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