diff --git a/src/display.ts b/src/display.ts index 72d4aed..bc37c8b 100644 --- a/src/display.ts +++ b/src/display.ts @@ -8,6 +8,7 @@ import { HEX, FeatureInfo, SvgMetadata, + RGBA, } from "./types"; // @ts-ignore import paper from "paper-jsdom"; @@ -24,28 +25,70 @@ const getChildElement = ( } }; +const swapSiblings = (svg: SVGSVGElement) => { + const shadowDOMElement = getChildElement(svg, "afterbegin") as SVGSVGElement; + const previousSibling = shadowDOMElement.nextSibling as SVGSVGElement; + if (previousSibling) { + svg.insertBefore(shadowDOMElement, previousSibling.nextElementSibling); + } +}; + +const addFaceShadowToBody = ( + face: FaceConfig, + insideSVG: SVGSVGElement, + faceOuterStrokePath: paper.Path, +) => { + const bodyGroup = getChildElement(insideSVG, "afterbegin"); + const bodySVG = paper.project.importSVG(bodyGroup); + const bodyColor = face.body.color; + const faceShadowPath = getShadowFromStroke( + faceOuterStrokePath, + bodyColor, + // "black", + ); + const faceShadowSVGString: string = paperPathToSVGString(faceShadowPath); + insideSVG.insertAdjacentHTML( + "afterbegin", + addWrapper(faceShadowSVGString, "shadow"), + ); + + const shadowSvgElement = getChildElementByClass( + insideSVG, + "shadow", + ) as SVGSVGElement; + + clipToParent( + shadowSvgElement, + bodySVG.clone(), + insideSVG, + "afterbegin", + "faceShadow", + ); + swapSiblings(insideSVG); +}; + const clipToParent = ( - fullSvg: SVGSVGElement, + childElement: SVGSVGElement, parentElement: paper.Path, + fullSvg: SVGSVGElement, insertLocation: "afterbegin" | "beforeend", + className: string, ) => { - const childElement = getChildElement( - fullSvg, - insertLocation, - ) as SVGSVGElement; const clippedItem = paper.project.importSVG(childElement); - fullSvg.removeChild(childElement); + childElement.remove(); const baseShape = unitePaths(findPathItems(parentElement.clone())); const smallChildren = findPathItems(clippedItem); + console.log("smallChildren", { smallChildren }); const childGroup = new paper.Group(); for (const child of smallChildren) { - child.stroke = null; - child.strokeWidth = 0; + // child.stroke = null; + // child.strokeWidth = 0; const intersection = baseShape.intersect(child); intersection.fillColor = child.fillColor; + intersection.stroke = child.stroke; intersection.strokeColor = child.strokeColor; intersection.strokeWidth = child.strokeWidth; intersection.opacity = child.opacity; @@ -62,7 +105,7 @@ const clipToParent = ( fullSvg, insertLocation, ) as SVGSVGElement; - addClassToElement(newlyAddedElement, "clipToParent"); + addClassToElement(newlyAddedElement, `${className}-clipToParent`); }; const findPathItems = (item: paper.Item): paper.PathItem[] => { @@ -112,6 +155,9 @@ const getOuterStroke = (svgElement: SVGElement): paper.Path => { unitedPath.strokeColor = new paper.Color("black"); unitedPath.strokeWidth = 6; unitedPath.fillColor = new paper.Color("transparent"); + unitedPath.miterLimit = 1; + + console.log("getOuterStroke", { importedItem, unitedPath, pathItems }); // Remove the imported item and its children from the project importedItem.remove(); @@ -132,9 +178,15 @@ const getShadowFromStroke = ( const shadowPath = svgElement.clone(); shadowPath.strokeWidth = 0; shadowPath.fillColor = new paper.Color(getSkinAccent(bodyColor)); - shadowPath.opacity = 0.33; + shadowPath.opacity = 0.25; shadowPath.width *= 0.75; shadowPath.position.y += 10; + console.log("shadowPath", { + shadowPath, + bodyColor, + svgElement, + shadowPathFill: shadowPath.fillColor, + }); return shadowPath; }; @@ -284,6 +336,32 @@ const getSkinAccent = (skinColor: string): string => { return rgbToHex(hslToRgb(hsl)); }; +const rgbaToRgbaString = (rgba: RGB | null, opacity: number): string => { + if (!rgba) { + return "rgba(0, 0, 0, 0)"; + } + return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${opacity})`; +}; + +const rgbaStringToRgba = (rgbaString: string): RGBA => { + const rgba = rgbaString.split("(")[1].split(")")[0].split(","); + return { + r: parseInt(rgba[0]), + g: parseInt(rgba[1]), + b: parseInt(rgba[2]), + a: parseInt(rgba[3]), + }; +}; + +const getSkinShadow = (skinColor: string): string => { + const skinColorRgba = rgbaToRgbaString( + hexToRgb(getSkinAccent(skinColor)), + 0.3, + ); + + return skinColorRgba; +}; + const getHairAccent = (hairColor: string): string => { const hsl = hexToHsl(hairColor); if (!hsl) { @@ -296,6 +374,18 @@ const getHairAccent = (hairColor: string): string => { } }; +const getChildElementByClass = ( + parentElement: SVGSVGElement, + className: string, +): SVGSVGElement | null => { + for (const child of parentElement.children) { + if (child.getAttribute("class") === className) { + return child as SVGSVGElement; + } + } + return null; +}; + const addClassToElement = (element: SVGGraphicsElement, className: string) => { const existingClass = element.getAttribute("class"); const existingClassSet = new Set(existingClass?.split(" ") || []); @@ -376,7 +466,7 @@ const scaleCentered = (element: SVGGraphicsElement, x: number, y: number) => { const tx = (cx * (1 - x)) / x; const ty = (cy * (1 - y)) / y; - addTransform(element, `scale(${x} ${y}) translate(${tx} ${ty})`); + addTransform(element, `scale(${x} ${y}) translate(${tx || 0} ${ty || 0})`); // Keep apparent stroke width constant, similar to how Raphael does it (I think) if ( @@ -403,7 +493,7 @@ const scaleTopDown = (element: SVGGraphicsElement, scaleY: number) => { ty *= 1.5; } - addTransform(element, `scale(${1} ${scaleY}) translate(0 ${ty})`); + addTransform(element, `scale(${1} ${scaleY}) translate(0 ${ty || 0})`); }; // Translate element such that its center is at (x, y). Specifying xAlign and yAlign can instead make (x, y) the left/right and top/bottom. @@ -432,7 +522,7 @@ const translate = ( cy = bbox.y + bbox.height / 2; } - addTransform(element, `translate(${x - cx} ${y - cy})`); + addTransform(element, `translate(${x - cx || 0} ${y - cy || 0})`); }; // Defines the range of fat/skinny, relative to the original width of the default head. @@ -572,11 +662,28 @@ const drawFeature = ( ); } - featureSVGString = featureSVGString.replace( - /\$\[shaveOpacity\]/g, - // @ts-ignore - feature.shaveOpacity || 0, - ); + if (featureSVGString.includes("$[skinShadow]")) { + const skinShadow = getSkinShadow(face.body.color); + featureSVGString = featureSVGString.replace( + /\$\[skinShadow\]/g, + skinShadow, + ); + } + + if (featureSVGString.includes("$[shaveOpacity]")) { + let opacity; + // Backwards compatibility + if (feature.shave) { + opacity = rgbaStringToRgba(feature.shave).a; + } else { + opacity = feature.shaveOpacity; + } + + featureSVGString = featureSVGString.replace( + /\$\[shaveOpacity\]/g, + opacity || 0, + ); + } featureSVGString = featureSVGString.replace(/\$\[headShave\]/g, "none"); @@ -625,8 +732,8 @@ const drawFeature = ( translate( childElement as SVGGraphicsElement, - position[0], - position[1], + position[0] || 0, + position[1] || 0, xAlign, ); } @@ -676,7 +783,7 @@ const drawFeature = ( // Scale individual feature relative to the edge of the head. If fatness is 1, then there are 47 pixels on each side. If fatness is 0, then there are 78 pixels on each side. const distance = (78 - 47) * (1 - face.fatness); // @ts-ignore - translate(childElement, distance, 0, "left", "top"); + translate(childElement, distance || 0, 0, "left", "top"); } if (info.name === "eye") { @@ -705,7 +812,7 @@ const drawFeature = ( translate( childElement as SVGGraphicsElement, 0, - +face.ear.size, + +face.ear.size || 0, "left", "top", ); @@ -728,7 +835,7 @@ const drawFeature = ( // @ts-ignore addTransform( childElement as SVGGraphicsElement, - `translate(0, ${-1 * face.eye.height})`, + `translate(0, ${-1 * face.eye.height || 0})`, ); } }; @@ -817,15 +924,15 @@ export const display = ( name: "nose", positions: [[200, 370]], }, + { + name: "mouth", + positions: [[200, 440]], + }, { name: "facialHair", positions: [null], scaleFatness: true, }, - { - name: "mouth", - positions: [[200, 440]], - }, { name: "hair", positions: [null], @@ -861,14 +968,13 @@ export const display = ( placeBeginning: true, }, { - name: "jersey", + name: "body", positions: [null], placeBeginning: true, }, { - name: "body", + name: "jersey", positions: [null], - placeBeginning: true, }, { name: "hairBg", @@ -879,7 +985,7 @@ export const display = ( ]; paper.setup(document.createElement("canvas")); - let baseFace: paper.Project; + let clipParent: paper.Project; let faceOuterStrokePath: paper.Path; for (const info of featureInfos) { @@ -898,51 +1004,80 @@ export const display = ( drawFeature(insideSVG, face, info, feature, metadata); - if (info.name == "head") { - baseFace = paper.project.importSVG(insideSVG); + if (info.name === "head") { + clipParent = paper.project.importSVG(insideSVG); + } else if (info.name === "body") { + clipParent = paper.project.importSVG( + getChildElement(insideSVG, "afterbegin"), + ); } if (metadata?.clip) { - clipToParent(insideSVG, baseFace.clone(), "beforeend"); + if (info.name === "jersey") { + const bodySVGElement = getChildElementByClass(insideSVG, "body"); + let jerseySVGElement = getChildElementByClass(insideSVG, "jersey"); + + if (!bodySVGElement || !jerseySVGElement) { + continue; + } + + const bodyStrokePath = getOuterStroke(bodySVGElement); + clipToParent( + jerseySVGElement, + bodyStrokePath, + insideSVG, + "afterbegin", + "jersey", + ); + + const bodyStrokeSVGString = paperPathToSVGString(bodyStrokePath); + insideSVG.insertAdjacentHTML( + "afterbegin", + addWrapper(bodyStrokeSVGString, "bodyStroke"), + ); + + jerseySVGElement = getChildElementByClass( + insideSVG, + "jersey-clipToParent", + ); + + if (!jerseySVGElement) { + continue; + } + + jerseySVGElement.remove(); + insideSVG.insertAdjacentElement("afterbegin", jerseySVGElement); + + bodySVGElement.remove(); + insideSVG.insertAdjacentElement("afterbegin", bodySVGElement); + } else { + const childElement = getChildElement( + insideSVG, + "beforeend", + ) as SVGSVGElement; + clipToParent( + childElement, + clipParent.clone(), + insideSVG, + "beforeend", + info.name, + ); + } } // After we add hair (which is last feature on face), add outer stroke to wrap entire face if (info.name == "hair") { faceOuterStrokePath = getOuterStroke(insideSVG); - let faceOuterStrokeSVGString = paperPathToSVGString(faceOuterStrokePath); + const faceOuterStrokeSVGString = + paperPathToSVGString(faceOuterStrokePath); insideSVG.insertAdjacentHTML( "beforeend", addWrapper(faceOuterStrokeSVGString, "outerStroke"), ); } - // Add shadow to body after face is made if (info.name === "body") { - const bodyGroup = getChildElement(insideSVG, "afterbegin"); - const bodySVG = paper.project.importSVG(bodyGroup); - const bodyColor = face.body.color; - const faceShadowPath = getShadowFromStroke( - faceOuterStrokePath, - bodyColor, - ); - const faceShadowSVGString: string = paperPathToSVGString(faceShadowPath); - insideSVG.insertAdjacentHTML( - "afterbegin", - addWrapper(faceShadowSVGString, "shadow"), - ); - - clipToParent(insideSVG, bodySVG.clone(), "afterbegin"); - const shadowDOMElement = getChildElement( - insideSVG, - "afterbegin", - ) as SVGSVGElement; - const previousSibling = shadowDOMElement.nextSibling as SVGSVGElement; - if (previousSibling) { - insideSVG.insertBefore( - shadowDOMElement, - previousSibling.nextElementSibling, - ); - } + addFaceShadowToBody(face, insideSVG, faceOuterStrokePath); } } diff --git a/src/types.ts b/src/types.ts index 06e3404..c928ac2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,6 +69,8 @@ export type RGB = { b: number; }; +export type RGBA = RGB & { a: number }; + export type HEX = string; export type SvgMetadata = { diff --git a/svgs/body/body3.svg b/svgs/body/body3.svg index c23900b..60f1064 100644 --- a/svgs/body/body3.svg +++ b/svgs/body/body3.svg @@ -1,8 +1,8 @@ - body3-svg - - - - - - \ No newline at end of file + + + + + + + diff --git a/svgs/body/body4.svg b/svgs/body/body4.svg index 20d5fe0..1c2ab59 100644 --- a/svgs/body/body4.svg +++ b/svgs/body/body4.svg @@ -1,5 +1,5 @@ - - - - - \ No newline at end of file + + + + + diff --git a/svgs/ear/ear1.svg b/svgs/ear/ear1.svg index e1e80f7..086d9e9 100644 --- a/svgs/ear/ear1.svg +++ b/svgs/ear/ear1.svg @@ -1,3 +1,7 @@ - - - \ No newline at end of file + + + + + + + diff --git a/svgs/ear/ear2.svg b/svgs/ear/ear2.svg index 7a52a7c..eb48b7b 100644 --- a/svgs/ear/ear2.svg +++ b/svgs/ear/ear2.svg @@ -1,3 +1,7 @@ - - - \ No newline at end of file + + + + + + + diff --git a/svgs/ear/ear3.svg b/svgs/ear/ear3.svg index 0f29daa..fbd73dd 100644 --- a/svgs/ear/ear3.svg +++ b/svgs/ear/ear3.svg @@ -1,3 +1,7 @@ - - - \ No newline at end of file + + + + + + + diff --git a/svgs/jersey/basketball-panel.svg b/svgs/jersey/basketball-panel.svg index 6ba8595..0458d2f 100644 --- a/svgs/jersey/basketball-panel.svg +++ b/svgs/jersey/basketball-panel.svg @@ -1,9 +1,9 @@ - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/svgs/jersey/basketball-standard.svg b/svgs/jersey/basketball-standard.svg index afd5fe8..cab1566 100644 --- a/svgs/jersey/basketball-standard.svg +++ b/svgs/jersey/basketball-standard.svg @@ -1,3 +1,3 @@ - - + + diff --git a/tools/lib/svg-metadata.js b/tools/lib/svg-metadata.js index 6dfcd58..ce3cece 100644 --- a/tools/lib/svg-metadata.js +++ b/tools/lib/svg-metadata.js @@ -333,26 +333,31 @@ export const svgMetadata = { gender: "both", sport: "basketball", legacyName: "jersey", + clip: true, }, "basketball-stripe": { gender: "both", sport: "basketball", legacyName: "jersey2", + clip: true, }, "basketball-stripe-2": { gender: "both", sport: "basketball", legacyName: "jersey4", + clip: true, }, "basketball-pinstripe": { gender: "both", sport: "basketball", legacyName: "jersey5", + clip: true, }, "basketball-panel": { gender: "both", sport: "basketball", legacyName: "jersey3", + clip: true, }, referee: { gender: "both", sport: "referee" }, suit: { gender: "both", sport: "suit" },