Skip to content

Commit

Permalink
Add gradient support
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 committed Oct 7, 2021
1 parent 4f09fa1 commit 4d3262e
Show file tree
Hide file tree
Showing 11 changed files with 1,880 additions and 250 deletions.
203 changes: 191 additions & 12 deletions docs/beta/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ module.exports={
const copy = require('copy-to-clipboard');
const createViewer = require('..');
const { svgElementToSvgImageContent } = require('../util');
const meshJson = require('../beta-fox.json');
const meshJson = require('./beta-fox.json');

document.addEventListener('keypress', function (event) {
if (event.keyCode === 99) {
Expand All @@ -360,7 +360,7 @@ createViewer({
meshJson,
});

},{"..":4,"../beta-fox.json":1,"../util":14,"copy-to-clipboard":5}],3:[function(require,module,exports){
},{"..":4,"../util":14,"./beta-fox.json":1,"copy-to-clipboard":5}],3:[function(require,module,exports){
module.exports={
"positions": [
[111.024597, 52.604599, 46.225899],
Expand Down Expand Up @@ -693,20 +693,24 @@ const {
createModelRenderer,
createNode,
setAttribute,
setGradientDefinitions,
} = require('./util');

module.exports = createLogo;

function createLogo(options = {}) {
const cameraDistance = options.cameraDistance || 400;
const { height, width } = calculateSizingOptions(options);
const meshJson = options.meshJson || foxJson;

const container = createNode('svg');
setAttribute(container, 'width', `${width}px`);
setAttribute(container, 'height', `${height}px`);
document.body.appendChild(container);

const modelObj = loadModelFromJson(options.meshJson || foxJson);
setGradientDefinitions(container, meshJson.gradients);

const modelObj = loadModelFromJson(meshJson);
const renderFox = createModelRenderer(container, cameraDistance, modelObj);
const renderScene = (lookCurrent, slowDrift) => {
const rect = container.getBoundingClientRect();
Expand Down Expand Up @@ -1202,6 +1206,7 @@ module.exports = {
createFaceUpdater,
createNode,
setAttribute,
setGradientDefinitions,
svgElementToSvgImageContent,
Polygon,
};
Expand Down Expand Up @@ -1396,7 +1401,9 @@ function createPolygonsFromModelJson(modelJson, createSvgPolygon) {
const polygonsByChunk = modelJson.chunks.map((chunk) => {
const { faces } = chunk;
return faces.map((face) => {
const svgPolygon = createSvgPolygon(chunk);
const svgPolygon = createSvgPolygon(chunk, {
gradients: modelJson.gradients,
});
const polygon = new Polygon(svgPolygon, face);
polygons.push(polygon);
return polygon;
Expand All @@ -1405,11 +1412,23 @@ function createPolygonsFromModelJson(modelJson, createSvgPolygon) {
return { polygons, polygonsByChunk };
}

function createStandardModelPolygon(chunk) {
const color = `rgb(${chunk.color})`;
function createStandardModelPolygon(chunk, { gradients = {} }) {
const svgPolygon = createNode('polygon');
setAttribute(svgPolygon, 'fill', color);
setAttribute(svgPolygon, 'stroke', color);

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 @@ -1555,10 +1574,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 @@ -1602,4 +1621,164 @@ function Polygon(svg, indices) {
this.zIndex = 0;
}

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 = ['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 gradent 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":7,"gl-mat4/lookAt":8,"gl-mat4/multiply":9,"gl-mat4/perspective":10,"gl-mat4/rotate":11,"gl-vec3/transformMat4":12}]},{},[2]);
Loading

0 comments on commit 4d3262e

Please sign in to comment.