diff --git a/flask-fox.json b/flask-fox.json new file mode 100644 index 00000000..3caf74fd --- /dev/null +++ b/flask-fox.json @@ -0,0 +1,603 @@ +{ + "positions": [ + [111.0246, 52.6046, 46.2259], + [114.025, 87.6733, 58.9818], + [66.192, 80.898, 55.3943], + [72.1133, 35.4918, 30.8714], + [97.8045, 116.561, 73.9788], + [16.7623, 58.0109, 58.0782], + [52.6089, 30.3641, 42.5561], + [106.8814, 31.9455, 46.9133], + [113.4846, 38.6049, 49.1215], + [108.6633, 43.2332, 46.3154], + [101.2166, 15.9822, 46.3082], + [16.6605, -16.2883, 93.6187], + [40.775, -10.2288, 85.2764], + [23.9269, -2.5103, 86.7365], + [11.1691, -7.0037, 99.3776], + [9.5692, -34.3939, 141.672], + [12.596, 7.1655, 88.741], + [61.1809, 8.8142, 76.9968], + [39.7195, -28.9271, 88.9638], + [13.7962, -68.5757, 132.057], + [15.2674, -62.32, 129.688], + [14.8446, -52.6096, 140.113], + [12.8917, -49.7716, 144.741], + [35.6042, -71.758, 81.0639], + [47.4625, -68.6061, 63.3697], + [38.2486, -64.7302, 38.9099], + [-12.8917, -49.7716, 144.741], + [-13.7962, -68.5757, 132.057], + [17.8021, -71.758, 81.0639], + [19.1243, -69.0168, 49.4201], + [38.2486, -66.2756, 17.7762], + [12.8928, -36.7035, 141.672], + [109.284, -93.5899, 27.8243], + [122.118, -36.8894, 35.025], + [67.7668, -30.197, 78.4178], + [33.1807, 101.852, 25.3186], + [9.4063, -35.5898, 150.722], + [-9.5692, -34.3939, 141.672], + [-9.4063, -35.5898, 150.722], + [11.4565, -37.8994, 150.722], + [-12.596, 7.1655, 88.741], + [-11.1691, -7.0037, 99.3776], + [70.2365, 62.8362, -3.9475], + [47.2634, 54.294, -27.4148], + [28.7302, 91.7311, -24.9726], + [69.1676, 6.5862, -12.7757], + [28.7302, 49.1003, -48.3596], + [31.903, 5.692, -47.822], + [35.0758, -34.4329, -16.2809], + [115.2841, 48.6815, 48.6841], + [110.8428, 28.4821, 49.1762], + [-19.1243, -69.0168, 49.4201], + [-38.2486, -66.2756, 17.7762], + [-111.0246, 52.6046, 46.2259], + [-72.1133, 35.4918, 30.8714], + [-66.192, 80.898, 55.3943], + [-114.025, 87.6733, 58.9818], + [-97.8045, 116.561, 73.9788], + [-52.6089, 30.3641, 42.5561], + [-16.7623, 58.0109, 58.0782], + [-106.8814, 31.9455, 46.9133], + [-108.6633, 43.2332, 46.3154], + [-113.4846, 38.6049, 49.1215], + [-101.2166, 15.9822, 46.3082], + [-16.6605, -16.2883, 93.6187], + [-23.9269, -2.5103, 86.7365], + [-40.775, -10.2288, 85.2764], + [-61.1809, 8.8142, 76.9968], + [-39.7195, -28.9271, 88.9638], + [-14.8446, -52.6096, 140.113], + [-15.2674, -62.32, 129.688], + [-47.4625, -68.6061, 63.3697], + [-35.6042, -71.758, 81.0639], + [-38.2486, -64.7302, 38.9099], + [-17.8021, -71.758, 81.0639], + [-12.8928, -36.7035, 141.672], + [-67.7668, -30.197, 78.4178], + [-122.118, -36.8894, 35.025], + [-109.284, -93.5899, 27.8243], + [-33.1807, 101.852, 25.3186], + [-11.4565, -37.8994, 150.722], + [-70.2365, 62.8362, -3.9475], + [-28.7302, 91.7311, -24.9726], + [-47.2634, 54.294, -27.4148], + [-69.1676, 6.5862, -12.7757], + [-28.7302, 49.1003, -48.3596], + [-31.903, 5.692, -47.822], + [-35.0758, -34.4329, -16.2809], + [-115.2841, 48.6815, 48.6841], + [-110.8428, 28.4821, 49.1762] + ], + "chunks": [ + { + "color": [123, 129, 255], + "faces": [ + [17, 33, 10], + [17, 18, 34], + [34, 33, 17], + [10, 6, 17], + [11, 15, 31], + [31, 18, 11], + [18, 12, 11], + [14, 16, 40], + [40, 41, 14], + [59, 5, 35], + [35, 79, 59], + [67, 63, 77], + [67, 77, 76], + [76, 68, 67], + [63, 67, 58], + [64, 68, 75], + [75, 37, 64], + [68, 64, 66], + [14, 41, 37], + [37, 15, 14], + [5, 59, 40], + [40, 16, 5] + ], + "mask": "mask1", + "name": "middle forehead, cheecks, below eyes, top of right ear", + "gradient": "paint1_linear" + }, + { + "color": [123, 129, 255], + "faces": [ + [31, 24, 18], + [6, 5, 16], + [16, 17, 6], + [24, 32, 33], + [33, 34, 24], + [5, 4, 35], + [75, 68, 71], + [58, 67, 40], + [40, 59, 58], + [71, 76, 77], + [77, 78, 71] + ], + "mask": "mask1", + "name": "cheek fluff, forehead", + "gradient": "paint10_linear" + }, + { + "color": [118, 61, 22], + "faces": [ + [0, 1, 2], + [2, 3, 0], + [4, 5, 2], + [6, 3, 2], + [2, 5, 6], + [7, 8, 9], + [10, 3, 6], + [10, 50, 7], + [7, 3, 10], + [7, 9, 3], + [49, 0, 9], + [3, 9, 0], + [53, 54, 55], + [55, 56, 53], + [57, 56, 55], + [58, 59, 55], + [55, 54, 58], + [60, 61, 62], + [63, 58, 54], + [63, 60, 89], + [60, 63, 54], + [60, 54, 61], + [88, 61, 53], + [54, 53, 61], + [2, 1, 4], + [55, 59, 57] + ], + "mask": "mask1", + "name": "ears", + "gradient": "paint4_linear" + }, + { + "color": [22, 22, 22], + "faces": [ + [36, 15, 37], + [37, 38, 36], + [31, 39, 22], + [22, 21, 31], + [31, 15, 36], + [36, 39, 31], + [75, 69, 26], + [26, 80, 75], + [75, 80, 38], + [38, 37, 75], + [38, 80, 39], + [39, 36, 38], + [39, 80, 26], + [26, 22, 39] + ], + "name": "nose" + }, + { + "color": [255, 159, 90], + "faces": [ + [21, 20, 24], + [24, 31, 21], + [69, 71, 70], + [71, 69, 75] + ], + "name": "upper chin" + }, + { + "color": [223, 117, 84], + "faces": [ + [19, 20, 21], + [21, 22, 19], + [20, 19, 23], + [23, 24, 20], + [23, 25, 24], + [19, 22, 26], + [26, 27, 19], + [23, 28, 29], + [23, 29, 30], + [25, 23, 30], + [29, 51, 52], + [52, 30, 29], + [27, 26, 69], + [69, 70, 27], + [70, 71, 72], + [72, 27, 70], + [72, 71, 73], + [51, 74, 72], + [52, 51, 72], + [73, 52, 72], + [19, 27, 74], + [74, 28, 19], + [51, 29, 28], + [28, 74, 51], + [74, 27, 72], + [28, 23, 19] + ], + "name": "chin" + }, + { + "color": [82, 88, 230], + "faces": [ + [24, 34, 18], + [16, 13, 12], + [12, 17, 16], + [13, 16, 11], + [71, 68, 76], + [40, 67, 66], + [66, 65, 40], + [65, 64, 40] + ], + "mask": "mask1", + "name": "cheeks and surrounding eyes" + }, + { + "color": [22, 22, 22], + "faces": [ + [11, 12, 13], + [64, 65, 66] + ], + "name": "eyes" + }, + { + "color": [123, 129, 255], + "faces": [ + [14, 15, 11], + [11, 16, 14], + [17, 12, 18], + [41, 64, 37], + [67, 68, 66] + ], + "mask": "mask1", + "name": "snout folds" + }, + { + "color": [97, 103, 245], + "faces": [ + [35, 4, 42], + [4, 1, 42], + [42, 43, 44], + [44, 35, 42], + [45, 43, 42], + [42, 10, 45], + [30, 32, 24], + [24, 25, 30], + [30, 33, 32], + [33, 30, 10], + [44, 43, 46], + [43, 45, 47], + [47, 46, 43], + [48, 47, 45], + [45, 30, 48], + [30, 45, 10], + [49, 42, 0], + [8, 7, 42], + [50, 42, 7], + [50, 10, 42], + [1, 0, 42], + [42, 9, 8], + [42, 49, 9], + [64, 41, 40], + [57, 59, 79], + [79, 81, 57], + [57, 81, 56], + [82, 79, 35], + [35, 44, 82], + [81, 79, 82], + [82, 83, 81], + [84, 63, 81], + [81, 83, 84], + [44, 46, 85], + [85, 82, 44], + [52, 73, 71], + [71, 78, 52], + [52, 78, 77], + [77, 63, 52], + [82, 85, 83], + [83, 85, 86], + [86, 84, 83], + [87, 52, 84], + [84, 86, 87], + [52, 63, 84], + [88, 53, 81], + [62, 81, 60], + [89, 60, 81], + [89, 81, 63], + [56, 81, 53], + [81, 62, 61], + [81, 61, 88], + [48, 87, 86], + [86, 47, 48], + [47, 86, 85], + [85, 46, 47], + [48, 30, 52], + [52, 87, 48] + ], + "mask": "mask1", + "name": "back, top of right ear" + } + ], + "gradients": { + "paint0_linear": { + "x1": "17.4996", + "y1": "4.16699", + "x2": "17.4996", + "y2": "26.4153", + "gradientUnits": "userSpaceOnUse", + "type": "linear", + "stops": [ + { + "stop-color": "#7E83FF" + }, + { + "offset": "1", + "stop-color": "#A1A5FF" + } + ] + }, + "paint1_linear": { + "type": "linear", + "x1": "29.3974", + "y1": "14.1221", + "x2": "29.3974", + "y2": "23.7337", + "gradientUnits": "userSpaceOnUse", + "stops": [ + { + "stop-color": "#555BE9" + }, + { + "offset": "1", + "stop-color": "#989CFF" + } + ] + }, + "paint2_linear": { + "x1": "5.6088", + "y1": "14.1221", + "x2": "5.6088", + "y2": "23.7337", + "gradientUnits": "userSpaceOnUse", + "type": "linear", + "stops": [ + { + "stop-color": "#555BE9" + }, + { + "offset": "1", + "stop-color": "#989CFF" + } + ] + }, + "paint3_linear": { + "x1": "24.3725", + "y1": "13.4755", + "x2": "36.9499", + "y2": "3.6613", + "gradientUnits": "userSpaceOnUse", + "type": "linear", + "stops": [ + { + "stop-color": "#070D9B" + }, + { + "offset": "1", + "stop-color": "#4248D6" + } + ] + }, + "paint4_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "10.6275", + "y1": "13.4755", + "x2": "-1.94986", + "y2": "3.6613", + "stops": [ + { + "stop-color": "#070D9B" + }, + { + "stop-color": "#4248D6", + "offset": "1" + } + ] + }, + "paint5_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "29.7202", + "y1": "23.6787", + "x2": "29.7202", + "y2": "31.2827", + "stops": [ + { + "stop-color": "#787878" + }, + { + "stop-color": "#5E5E5E", + "offset": "1" + } + ] + }, + "paint6_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "5.28661", + "y1": "23.6787", + "x2": "5.28661", + "y2": "31.2827", + "stops": [ + { + "stop-color": "#787878" + }, + { + "stop-color": "#5E5E5E", + "offset": "1" + } + ] + }, + "paint7_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "11.5473", + "y1": "9.81738", + "x2": "11.5473", + "y2": "17.7514", + "stops": [ + { + "stop-color": "#7A7A7A" + }, + { + "stop-color": "#949494", + "offset": "1" + } + ] + }, + "paint8_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "23.4521", + "y1": "9.81738", + "x2": "23.4521", + "y2": "17.7514", + "stops": [ + { + "stop-color": "#7A7A7A" + }, + { + "stop-color": "#949494", + "offset": "1" + } + ] + }, + "paint9_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "12.6116", + "y1": "32.3962", + "x2": "12.6116", + "y2": "-3.33511", + "stops": [ + { + "stop-color": "#7A7C7D" + }, + { + "stop-color": "#CECECF", + "offset": "1" + } + ] + }, + "paint10_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "22.3884", + "y1": "32.3962", + "x2": "22.3884", + "y2": "-3.33511", + "stops": [ + { + "stop-color": "#686EFC" + }, + { + "stop-color": "#C7C9FF", + "offset": "1" + } + ] + }, + "paint11_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "26.2118", + "y1": "16.7209", + "x2": "26.2118", + "y2": "30.8694", + "stops": [ + { + "stop-color": "#3E3E3E" + }, + { + "stop-color": "#616161", + "offset": "1" + } + ] + }, + "paint12_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "8.78767", + "y1": "16.7209", + "x2": "8.78767", + "y2": "30.8694", + "stops": [ + { + "stop-color": "#3E3E3E" + }, + { + "stop-color": "#616161", + "offset": "1" + } + ] + }, + "paint13_linear": { + "type": "linear", + "gradientUnits": "userSpaceOnUse", + "x1": "17.4382", + "y1": "4.12515", + "x2": "17.4382", + "y2": "34.1012", + "stops": [ + { + "stop-color": "#FF60DC" + }, + { + "stop-color": "#6B71FF", + "offset": "1" + } + ] + }, + "paint14_radial": { + "cx": "50%", + "cy": "0", + "r": "70%", + "gradientUnits": "userSpaceOnUse", + "type": "radial", + "stops": [ + { + "stop-color": "rgb(255,255,255)" + }, + { + "offset": "1", + "stop-color": "rbg(0,0,0)" + } + ] + } + }, + "masks": { + "mask1": { + "modelColor": "rgb(255,96,220)", + "maskColor": "url('#paint14_radial')" + } + } +} diff --git a/index.js b/index.js index fc8f64bd..0b99fe40 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ function createLogo(options = {}) { document.body.appendChild(container); setGradientDefinitions(container, meshJson.gradients); + setMaskDefinitions(container, meshJson.masks, height, width); const modelObj = loadModelFromJson(meshJson); const renderFox = createModelRenderer(container, cameraDistance, modelObj); @@ -36,3 +37,24 @@ function createLogo(options = {}) { Object.assign({ cameraDistance }, options), ); } + +function setMaskDefinitions(container, masks, height, width) { + for (const [maskId, maskDefinition] of Object.entries(masks)) { + const mask = createNode('mask'); + setAttribute(mask, 'id', maskId); + + const maskedRect = createNode('rect'); + + // Extend mask beyond container to ensure it completely covers the model. + // The model can extend beyond the container as well. + setAttribute(maskedRect, 'width', width * 1.5); + setAttribute(maskedRect, 'height', height * 1.5); + setAttribute(maskedRect, 'x', `-${Math.floor(width / 4)}`); + setAttribute(maskedRect, 'y', `-${Math.floor(height / 4)}`); + + setAttribute(maskedRect, 'fill', maskDefinition.maskColor); + mask.appendChild(maskedRect); + + container.appendChild(mask); + } +} diff --git a/util.js b/util.js index 027af70d..fcaeb143 100644 --- a/util.js +++ b/util.js @@ -269,7 +269,7 @@ function createModelRenderer(container, cameraDistance, modelObj) { return (rect, lookPos, slowDrift) => { const matrix = computeMatrix(rect, lookPos, slowDrift); updatePositions(matrix); - updateFaces(rect, container, polygons, transformed); + updateFaces(rect); }; } @@ -288,13 +288,25 @@ function positionsFromModel(positions, modelJson) { function createPolygonsFromModelJson(modelJson, createSvgPolygon) { const polygons = []; const polygonsByChunk = modelJson.chunks.map((chunk, index) => { - const { faces } = chunk; + const { faces, mask: maskId } = chunk; return faces.map((face) => { const svgPolygon = createSvgPolygon(chunk, { gradients: modelJson.gradients, index, }); - const polygon = new Polygon(svgPolygon, face); + let maskPolygon; + if (maskId) { + if (modelJson.masks[maskId] === undefined) { + throw new Error(`Unrecognized mask ID: '${maskId}'`); + } + + maskPolygon = createSvgPolygon(chunk, { + gradients: modelJson.gradients, + mask: modelJson.masks[maskId], + index, + }); + } + const polygon = new Polygon(svgPolygon, face, maskPolygon); polygons.push(polygon); return polygon; }); @@ -314,16 +326,21 @@ function createPolygonsFromModelJson(modelJson, createSvgPolygon) { * @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. + * @param {number} options.index - The index for the chunk this polygon is found in. + * @param {object} [options.mask] - The mask definition to use for this polygon. * @returns {Element} The `` SVG element. */ -function createStandardModelPolygon(chunk, { gradients = {}, index }) { +function createStandardModelPolygon(chunk, { gradients = {}, index, mask }) { const svgPolygon = createNode('polygon'); if (chunk.gradient && chunk.color) { throw new Error( `Both gradient and color for chunk '${index}'. These options are mutually exclusive.`, ); + } else if (mask) { + setAttribute(svgPolygon, 'mask', `url('#${chunk.mask}')`); + setAttribute(svgPolygon, 'fill', mask.modelColor); + setAttribute(svgPolygon, 'stroke', mask.modelColor); } else if (chunk.gradient) { const gradientId = chunk.gradient; if (!gradients[gradientId]) { @@ -461,7 +478,6 @@ function createFaceUpdater(container, polygons, transformed) { const points = []; let zmax = -Infinity; let zmin = Infinity; - const element = poly.svg; for (let j = 0; j < 3; ++j) { const idx = indices[j]; points.push( @@ -473,20 +489,26 @@ function createFaceUpdater(container, polygons, transformed) { zmax = Math.max(zmax, z); zmin = Math.min(zmin, z); } + poly.zIndex = zmax + 0.25 * zmin; const joinedPoints = points.join(' '); if (joinedPoints.indexOf('NaN') === -1) { - setAttribute(element, 'points', joinedPoints); + setAttribute(poly.svg, 'points', joinedPoints); + if (poly.maskSvg) { + setAttribute(poly.maskSvg, 'points', joinedPoints); + } } - toDraw.push(poly); } toDraw.sort(compareZ); - const newPolygons = toDraw.map((poly) => poly.svg); + const newPolygons = toDraw + .map((poly) => [poly.svg, ...(poly.maskSvg ? [poly.maskSvg] : [])]) + .flat(); const defs = container.getElementsByTagName('defs'); - container.replaceChildren(...defs, ...newPolygons); + const maskChildren = container.getElementsByTagName('mask'); + container.replaceChildren(...defs, ...maskChildren, ...newPolygons); }; } @@ -524,10 +546,13 @@ function svgElementToSvgImageContent(svgElement) { return content; } -function Polygon(svg, indices) { +function Polygon(svg, indices, maskSvg) { this.svg = svg; this.indices = indices; this.zIndex = 0; + if (maskSvg) { + this.maskSvg = maskSvg; + } } /**