diff --git a/includes/icons/icons_hair_blair.png b/includes/icons/icons_hair_blair.png index bad4cac24..7a02881ba 100644 Binary files a/includes/icons/icons_hair_blair.png and b/includes/icons/icons_hair_blair.png differ diff --git a/package.json b/package.json index 1c707afe8..10ab26857 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-react": "^7.18.0", "file-loader": "^5.0.2", "html-webpack-plugin": "^3.2.0", + "lodash": "^4.17.15", "mini-css-extract-plugin": "^0.9.0", "node-sass": "^4.13.1", "prettier": "^1.19.1", diff --git a/src/components/body-editor.js b/src/components/body-editor.js index 04b385c80..853e47a6e 100644 --- a/src/components/body-editor.js +++ b/src/components/body-editor.js @@ -3,6 +3,7 @@ import * as THREE from "three"; import EditorUtils from "./editor-utils"; import {EditorPage} from "./editor-page" import Buttons, {TextureButton} from "./buttons" +import Swatches from "./color-picker2" import PropTypes from "prop-types"; import { LabeledTexture } from "../labeled-texture"; @@ -15,7 +16,6 @@ import curvy from "../../includes/icons/icons_curvy.png" import skin from "../../includes/textures/skin_default.png"; import blush from "../../includes/textures/blush_default.png"; -import logo from "../../includes/textures/logo_front/ae.png" const skinColors = ["#503335", "#592f2a", "#a1665e", "#c58c85", "#d1a3a4", "#ecbcb4", "#FFE2DC"]; const blushColors = ["#551F25", "#82333C", "#983E38", "#DC6961"]; @@ -46,33 +46,45 @@ export default class BodyEditor extends Component{ render() { return ( - + +
- this.props.onChange(e.target.value)}/> - this.props.onChange(e.target.value)}/> -
- -
- { - this.setState({skin:e.rgb}); - EditorUtils.setMaterialColor(e.hex, this.materials[0]) + { + this.props.onChange((src == straight) ? 'straight' : 'curvy') + }} /> -
- +
- { - this.setState({blush:e.rgb}); - EditorUtils.setMaterialColor(e.hex, this.materials[1]) - }} - /> + { + EditorUtils.setMaterialColor(color, this.materials[0]) + }} + /> +
+ +
+ { + if (color == 'none') { + this.materials[1].setActive(false); + } else { + this.materials[1].setActive(true); + EditorUtils.setMaterialColor(color, this.materials[1]) + } + }} + />
); diff --git a/src/components/color-picker.js b/src/components/color-picker.js index 27c27a79a..ce7b90c21 100644 --- a/src/components/color-picker.js +++ b/src/components/color-picker.js @@ -2,9 +2,10 @@ import React from 'react' import { CustomPicker } from 'react-color' import { Hue, Saturation, Swatch } from 'react-color/lib/components/common' -import color from 'react-color/lib/helpers/color' +import * as COLOR from 'react-color/lib/helpers/color'; -export const ColorPicker = ({ hex, hsl, hsv, colors, onChange }) => { + +export const ColorPicker = ({ hex, hsl, hsv, colors, onChange, color}) => { const styles = { customContainer: { height: 50, @@ -37,44 +38,45 @@ export const ColorPicker = ({ hex, hsl, hsv, colors, onChange }) => { borderRadius: 4 }, } - const uh = (t) => { - return t; - } - const uhh = (e) => { - onChange(e) + const onClick = (e) => { + disabled = e.disabled; } - return ( -
-
-
- -
- -
-
- -
-
-
+ const customColorMenu = (e) => { + return (
+
+
+ +
+
+
+
+
+ +
+
) + } + return ( +
+ {}
- +
) } -const PresetColors = ({ colors, onChange = () => {}, onSwatchHover }) => { +const PresetColors = ({ colors, onChange = () => {}, onSwatchHover, color}) => { const styles = { swatch: { width: '30px', @@ -88,19 +90,27 @@ const PresetColors = ({ colors, onChange = () => {}, onSwatchHover }) => { } } + + var clr = null + const handleChange = (hexcode, e) => { - console.log(hexcode) - color.isValidHex(hexcode) && onChange({ + clr = hexcode; + onChange({ hex: hexcode, source:'hex' }, e) } + return (
{colors.map((c,i) => { var swatchStyle = {...styles.swatch}; swatchStyle.marginLeft = i === 0 ? '0' : swatchStyle.marginLeft; + + if (color.hex != undefined && color.hex.toLowerCase() == c.toLowerCase()) { + swatchStyle.boxShadow= `0 0 4px ${c}` + } return ( {this.props.onClick(this.props.color)}}> +
+ + ); + } +} + +class TextureSwatch extends Component { + constructor(props) { + super(props); + } + + render() { + var swatchStyle = (this.props.style) ? {...styles.swatch, ...this.props.style} : {...styles.swatch}; + + if (this.props.first) swatchStyle.marginLeft = '0px'; + + if (this.props.selected) swatchStyle.boxShadow = `0 0 4px grey` + + return( + {this.props.onClick(this.props.src)}}> +
+ +
+
+ ); + } +} + +class IconSwatch extends Component { + constructor(props) { + super(props); + this.file = React.createRef(); + } + + onClick() { + if (this.props.value == "upload") { + this.file.current.click(); + } + this.props.onClick(this.props.value); + } + onUpload(e) { + if (typeof this.props.onUpload == "function") { + + this.props.onUpload(e.target.files[0]); + } + } + + render() { + var swatchStyle = (this.props.style) ? {...styles.swatch, ...this.props.style} : {...styles.swatch}; + + if (this.props.first) swatchStyle.marginLeft = '0px'; + + if (this.props.selected) swatchStyle.boxShadow = `0 0 4px grey` + + return( + {this.onClick()}}> +
+ + {this.props.value == "upload" ? + + {this.onUpload(e)}} hidden/> + : null} +
+
+ ); + } +} + +class Disable extends Component { + constructor(props) { + super(props); + } + + render() { + var swatchStyle = (this.props.style) ? {...styles.swatch, ...this.props.style} : {...styles.swatch}; + + if (this.props.first) swatchStyle.marginLeft = '0px'; + + if (this.props.selected) swatchStyle.boxShadow = `0 0 4px grey` + + return( + {this.props.onClick('none')}}> +
+ +
+
+ ); + } +} + +export default class Swatches extends Component { + static propTypes = { + canDisable: PropTypes.bool, + canUpload: PropTypes.bool, + colors: PropTypes.array, + textures: PropTypes.array, + onChange: PropTypes.func, + onUpload: PropTypes.func, + width: PropTypes.string, + height: PropTypes.string, + selected: PropTypes.string + } + + static defaultProps = { + onChange: () => {}, + onUpload: () => {}, + colors: [], + textures: [], + selected: '' + } + + constructor(props) { + super(props); + this.state = {selected: this.props.selected} + this.size = {}; + + console.log(this.props.selected) + if (this.props.width) this.size.width = this.props.width; + if (this.props.height) this.size.height = this.props.height; + } + + onClickHandler(value) { + this.setState({selected: value}); + this.props.onChange(value); + } + + render() { + const canDisable = this.props.canDisable; + const canUpload = this.props.canUpload; + + return( +
+ {canDisable ? + + : null + } + + {canUpload ? + + : null + } + + {this.props.colors.map((c,i) => { + return( + + ); + })} + + {this.props.textures.map((s,i) => { + return( + + ); + })} +
+ ); + } + + +} \ No newline at end of file diff --git a/src/components/editor.js b/src/components/editor.js index aec47ad11..8a5f52483 100644 --- a/src/components/editor.js +++ b/src/components/editor.js @@ -1,21 +1,22 @@ import React, { Component } from "react"; import * as THREE from "three"; +import {GLTFExporter} from "three/examples/jsm/exporters/GLTFExporter" import PropTypes from "prop-types"; import {DisableButton, PresetColorButton, CustomColorButton, RadioButton,TextureButton, DownloadButton} from "./buttons" import Material from "./material" +import Swatches from "./color-picker2" import BodyEditor from "./body-editor" import HeadEditor from "./head-editor" import ShirtEditor from "./shirt-editor" -import JacketEditor from "./jacket-editor" import body from "../../includes/icons/icons_body.png" import head from "../../includes/icons/icons_head.png" import shirt from "../../includes/icons/icons_shirt.png" -import jacket from "../../includes/icons/icons_jacket.png" import "../stylesheets/editor" +import { until } from "../util"; export class Editor extends Component{ static propTypes = { @@ -74,7 +75,6 @@ export class Editor extends Component{ this.body.current.editorPage.current.setActive(value === "Body"); this.head.current.editorPage.current.setActive(value === "Head"); this.shirt.current.editorPage.current.setActive(value === "Shirt"); - this.jacket.current.editorPage.current.setActive(value === "Jacket"); } updateHairType(hairType) { @@ -89,7 +89,34 @@ export class Editor extends Component{ this.bodyType = bodyType; } - getDownloadTexture() { + getGLB() { + const exporter = new GLTFExporter(); + const fullscene = this.props.models[this.bodyType][this.hairType].fullscene; + console.log(this.props.models[this.bodyType][this.hairType].fullscene) + + new THREE.TextureLoader().load(this.getMergedTextureURL(), (tex) => { + fullscene.scene.traverse(node => { + if (node.name == "Body") { + node.material.map = tex; + node.material.map.flipY = false; + } + }); + + exporter.parse(this.props.models[this.bodyType][this.hairType].fullscene.scene, (gltf) => { + const blob = new Blob([gltf], {type: 'application/octet-stream'}); + const el = document.createElement("a"); + el.style.display = "none"; + el.href = URL.createObjectURL(blob); + el.download = "custom_avatar.glb" + el.click(); + el.remove(); + + }, { animations: this.props.models[this.bodyType][this.hairType].fullscene.animations, binary:true}) + + }, undefined, (e) => {console.log(e)}) + } + + getMergedTextureURL() { const canvas = window.document.createElement("canvas"); canvas.width = 1024; canvas.height = 1024; @@ -97,16 +124,25 @@ export class Editor extends Component{ const ctx = canvas.getContext("2d"); this.materials.forEach(mat => { - ctx.drawImage(mat.getDownloadTexture(), 0, 0); + ctx.drawImage(mat.getBakedTexture(), 0, 0); }); - new THREE.ImageLoader().load(canvas.toDataURL("image/png", 1.0), img => { + return canvas.toDataURL("image/png", 1.0); + + return ( new Promise((resolve,reject) => { + new THREE.ImageLoader().load(canvas.toDataURL("image/png", 1.0), resolve, undefined, reject) + } + )); + } + + downloadMergedTexture() { + new THREE.ImageLoader().load(this.getMergedTextureURL(), (r) => { const el = document.createElement("a"); - el.href = img.src; - el.download = "tex.png"; + el.href = r.src; + el.download = "custom_avatar_texture.png"; el.click(); el.remove(); - }) + }, undefined, undefined) } render() { @@ -114,17 +150,30 @@ export class Editor extends Component{ return( <>
- this.changePage(e.target.value)} src={body}/> - this.changePage(e.target.value)} src={head}/> - this.changePage(e.target.value)} src={shirt}/> - this.changePage(e.target.value)} src={jacket}/> + { + var loc = "" + switch(src) { + case body: loc = "Body"; break; + case head: loc = "Head"; break; + case shirt: loc = "Shirt"; break; + } + this.changePage(loc); + + }} + /> +
- this.updateBodyType(b)}/> - this.updateHairType(h)}/> + this.updateBodyType(b)} selected={this.bodyType}/> + this.updateHairType(h)} selected={this.hairType}/> - - + + ); } diff --git a/src/components/head-editor.js b/src/components/head-editor.js index c0e364151..dad89bcc6 100644 --- a/src/components/head-editor.js +++ b/src/components/head-editor.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import * as THREE from "three"; import ColorPicker from "./color-picker"; +import Swatches from "./color-picker2" import PropTypes from "prop-types"; @@ -61,10 +62,22 @@ export default class HeadEditor extends Component {
- this.onChangeHandler(e)}/> - this.onChangeHandler(e)}/> - this.onChangeHandler(e)}/> - this.onChangeHandler(e)}/> + { + switch(src) { + case hair_none: this.props.onChange('none'); break; + case hair_short: this.props.onChange('short'); break; + case hair_blair: this.props.onChange('blair'); break; + case hair_long: this.props.onChange('long'); break; + } + }} + /> +
@@ -72,7 +85,7 @@ export default class HeadEditor extends Component { color={this.state.hair} colors={hairColors} onChange={(e) => { - this.setState({hair:e.rgb}); + this.setState({hair:e}); EditorUtils.setMaterialColor(e.hex, this.materials[0]) EditorUtils.setMaterialColor(e.hex, this.materials[1]) }} @@ -84,7 +97,7 @@ export default class HeadEditor extends Component { color={this.state.eye} colors={eyeColors} onChange={(e) => { - this.setState({eye:e.rgb}); + this.setState({eye:e}); EditorUtils.setMaterialColor(e.hex, this.materials[2]) }} /> diff --git a/src/components/jacket-editor.js b/src/components/jacket-editor.js index 2fc3f0faa..cb049c0dc 100644 --- a/src/components/jacket-editor.js +++ b/src/components/jacket-editor.js @@ -1,15 +1,44 @@ import React, { Component } from "react"; +import * as THREE from "three"; + +import ColorPicker from "./color-picker"; +import {DisableButton, PresetColorButton, CustomColorButton, TextureButton} from "./buttons" + import {EditorPage} from "./editor-page" -import {DisableButton, PresetColorButton, CustomColorButton} from "./buttons" +import EditorUtils from "./editor-utils" + +import { LabeledTexture } from "../labeled-texture"; +import Material from "./material" + +import jacket from "../../includes/textures/jacket_default.png" + +import ae from "../../includes/textures/logo_front/ae.png"; +import duck from "../../includes/textures/logo_front/duck.png"; +import gt from "../../includes/textures/logo_front/gt.png"; + +const jacketColors = ["#7d0c1e", "#cedded", "#92a1b1", "#3479b7"]; export default class JacketEditor extends Component { constructor(props) { super(props); + + this.materials = [ + new Material(this.props.model.material.clone(), "jacket", [new LabeledTexture(jacket)]), + new Material(this.props.model.material.clone(), "jacketLogoBack", + [new LabeledTexture(duck),new LabeledTexture(ae),new LabeledTexture(gt)], false, true, 662,476, + 220,270, true) + ] + + this.state = { + jacketColor: new THREE.Color().randomize().getHexStringFull() + } + this.editorPage = React.createRef(); } - setActive(isActive) { - this.editorPage.current.setActive(isActive); + setLogo(index, src) { + this.materials[index].setActive(src!=null); + (src == null) ? null : this.materials[index].setTextureByPath(src) ; } render() { @@ -18,12 +47,16 @@ export default class JacketEditor extends Component {
- - - - + { + this.setState({jacketColor:e.rgb}); + EditorUtils.setMaterialColor(e.hex, this.materials[0]) + }} + />
- +
diff --git a/src/components/material.js b/src/components/material.js index 3714ce780..0fcd6aa2c 100644 --- a/src/components/material.js +++ b/src/components/material.js @@ -30,18 +30,10 @@ export default class Material { this.material.needsUpdate = true; } - setTextureByPath(path) { - const index = this.labeledTextures.findIndex((element) => { - return element.path == path; - }); - - setTexture(index); - } setTexture(index) { if (index >= this.labeledTextures.length || index < 0) return; - this.labeledTextures[index] .getTexture( this.x, @@ -50,11 +42,18 @@ export default class Material { this.height, this.scaleTexture ).then(texture => { - this.material.visible = this.active; - this.material.needsUpdate = true; - this.material.map = texture; - this.index = index; + this.material.visible = this.active; + this.material.needsUpdate = true; + this.material.map = texture; + this.index = index; + }); + } + + setTextureByPath(path) { + const index = this.labeledTextures.findIndex((element) => { + return element.src == path; }); + this.setTexture(index); } getTexture() { @@ -62,7 +61,7 @@ export default class Material { this.scaleTexture); } - getDownloadTexture() { + getBakedTexture() { //get current texture image const img = this.material.map.image; const clr = this.material.color; diff --git a/src/components/shirt-editor.js b/src/components/shirt-editor.js index e3ce27431..1bdc0f5df 100644 --- a/src/components/shirt-editor.js +++ b/src/components/shirt-editor.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import * as THREE from "three"; import ColorPicker from "./color-picker"; +import Swatches from "./color-picker2" import {EditorPage} from "./editor-page" import EditorUtils from "./editor-utils" @@ -9,6 +10,7 @@ import { LabeledTexture } from "../labeled-texture"; import Material from "./material" import shirt from "../../includes/textures/shirt_default.png"; +import jacket from "../../includes/textures/jacket_default.png"; import ae from "../../includes/textures/logo_front/ae.png"; import duck from "../../includes/textures/logo_front/duck.png"; @@ -24,51 +26,106 @@ export default class ShirtEditor extends Component { this.materials = [ new Material(this.props.model.material.clone(), "shirt", [new LabeledTexture(shirt)]), - new Material(this.props.model.material.clone(), "shirtfrontlogo", - [new LabeledTexture(duck),new LabeledTexture(ae),new LabeledTexture(gt)], false, true, 148,476, - 220,270, true) + + new Material(this.props.model.material.clone(), "logoFront", + [new LabeledTexture(duck),new LabeledTexture(ae),new LabeledTexture(gt)], false, true, 148,476, + 220,270, true), + new Material(this.props.model.material.clone(), "jacket", [new LabeledTexture(jacket)]), + new Material(this.props.model.material.clone(), "logoBack", + [new LabeledTexture(duck),new LabeledTexture(ae),new LabeledTexture(gt)], false, true, 662,476, + 220,270, true), ] this.state = { shirt:new THREE.Color().randomize().getHexStringFull(), + jacket:new THREE.Color().randomize().getHexStringFull(), } EditorUtils.setMaterialColor(this.state.shirt, this.materials[0]) this.editorPage = React.createRef(); - //this.setLogo(); } - setLogo() { - this.materials[0].setTexture(0); + setLogo(index, src) { + this.materials[index].setActive(src!=null); + (src == null) ? null : this.materials[index].setTextureByPath(src) ; } render() { return ( - +
- { - this.setState({hair:e.rgb}); - EditorUtils.setMaterialColor(e.hex, this.materials[0]) + { + EditorUtils.setMaterialColor(color, this.materials[0]) + }} + /> +
+ +
+ { + if (color == 'none') { + this.materials[2].setActive(false); + } else { + this.materials[2].setActive(true); + EditorUtils.setMaterialColor(color, this.materials[2]) + } }} /> +
- - - - + + { + if (src == 'none') { + this.setLogo(1, null); + } else { + this.setLogo(1, src) + } + }} + onUpload={(file) => { + const path = window.URL.createObjectURL(file); + this.materials[1].labeledTextures.push(new LabeledTexture(path)) + console.log(this.materials[1].labeledTextures) + this.setLogo(1, path) + }} + />
- - - - + { + if (src == 'none') { + this.setLogo(3, null); + } else { + this.setLogo(3, src) + } + }} + onUpload={(file) => { + const path = window.URL.createObjectURL(file); + this.materials[3].labeledTextures.push(new LabeledTexture(path)) + console.log(this.materials[3].labeledTextures) + this.setLogo(3, path) + }} + /> +
); diff --git a/src/labeled-texture.js b/src/labeled-texture.js index b47bf26f0..c3d9987fc 100644 --- a/src/labeled-texture.js +++ b/src/labeled-texture.js @@ -4,6 +4,7 @@ export class LabeledTexture { constructor(path, label = "Default", autoLoad = false) { this.label = label; this.path = path; + this.src = path; this.loaded = undefined; if (autoLoad) this.getTexture(path); } diff --git a/src/main.js b/src/main.js index 817c3b308..00f7c3457 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import * as THREE from "three"; import * as React from "react"; import * as ReactDOM from "react-dom"; +import { cloneDeep } from "lodash" import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import styles from "./stylesheets/main.scss"; @@ -87,8 +88,11 @@ async function init() { function processModel(src, bodyType, hair, val, success) { var mod = null; + var fullscene = null; if (success) { + fullscene = cloneDeep(val) + val.scene.scale.x = 30; val.scene.scale.y = 30; val.scene.scale.z = 30; @@ -105,9 +109,9 @@ async function init() { } if (models[bodyType]) { - models[bodyType][hair] = {model: mod, src:src} + models[bodyType][hair] = {model: mod, fullscene: fullscene} } else { - models[bodyType] = {[hair]: {model: mod, src:src}} + models[bodyType] = {[hair]: {model: mod, fullscene: fullscene}} } cnt+=1; } diff --git a/src/stylesheets/editor.scss b/src/stylesheets/editor.scss index 33bcae733..f955740e4 100644 --- a/src/stylesheets/editor.scss +++ b/src/stylesheets/editor.scss @@ -7,10 +7,17 @@ .editorPage { height: 100%; padding: 20px; + padding-bottom: 0; display: flex; flex-direction: column; justify-content: start; align-items: center; //background-color: blue; + &>div { + margin-bottom: 10px; + } + &>label { + margin-bottom: 5px; + } } diff --git a/src/stylesheets/main.scss b/src/stylesheets/main.scss index 3c5f26205..563d89908 100644 --- a/src/stylesheets/main.scss +++ b/src/stylesheets/main.scss @@ -1,9 +1,23 @@ body { font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + display: flex; + justify-content: center; } #container { + margin-top: 30px; height: 400px; + max-width: 900px; + display: flex; + flex-direction: row; + align-content: center; + justify-content: space-between; + width: 100%; + + @media screen and (max-width: 900px) and (max-device-width: 900px){ + flex-direction: column; + } } td { @@ -17,10 +31,11 @@ td { } #options { - display: inline-block; + display: inline-flex; + flex-direction: column; vertical-align: top; height: 100%; - + width: 360px; .texture-layer { height: 32px;