Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
	<div>
		<h1>SIMPLE MODEL RENDERER</h1>
		<label>Blockbench Model <lavel class="detail">(Block model only)</lavel>:</label>
		<input id="bbmodel" name="bbmodel" type="file" accept=".bbmodel"/>
    <div class="model_files">
      <span>Model files:</span><br>
      <span class="desc">Download models. When model loaded the shape will change.</span>
      <ul>
      <li><a href="https://www.mediafire.com/file/h2e5w9m06itx4ih/slab.bbmodel/file" target="_blank">Slab</a></li>
      <li><a href="https://www.mediafire.com/file/of2bzgb6g0dg6ae/stairs.bbmodel/file" target="_blank">Stairs</a></li>
      <li><a href="https://www.mediafire.com/file/l4sro7e1ib6bq2y/template_wall.bbmodel/file" target="_blank">Wall</a></li>
      <li><a href="https://www.mediafire.com/file/qxnde6zxmr92rlc/models.zip/file" target="_blank">Other(zip)</a></li>
    </ul></div>
		<p class="caption">PREVIEW:</p>
		<div id="outbox">
			<div id="background">
				<canvas id="myCanvas"></canvas>
			</div>
			<div id="controls">
				<div class="caption">TEXTURES:</div>
				<div id="list"></div>
				<button id="mrotate">Rotate Model</button><button id="reset">Reset Camera</button><button id="draw">Capture Image</button><br>
				<p id="apng_msg"></p>
				<label>Zoom:</label><input id="zoom" value="1"/><label>Level:</label><input id="level" value="0"/><br>
				<div class="caption">ANIMATED TEXTURE SETTINGS:</div>
				<label>Interval:</label><input id="interval" value="25" readonly/><label id="finfo">(33.33fps, 0.3 seconds)</label><br>
				<label>Num of frame:</label><input id="fcount" value="1" readonly/>
				<label>Fade division:</label><input id="fdiv" value="1" readonly/><br>
				<button id="create_apng">Create APNG</button><br>
				<div id="anim"></div>
			</div>
		</div>
	</div>
  
	<img id="missing" src="data:image/gif;base64,R0lGODlhAgACAJEAAAAAAAAAAP8A/wAAACH5BAUKAAAALAAAAAACAAIAAAIDjBQFADs=" style="display:none" />
	<p id="msg" class="caption">CAPTURED IMAGE <label class="detail">(Right-click to save with context menu.)</label>:</p>
	<img id="drawed" />

              
            
!

CSS

              
                
		body {
			background-color: #242424;
			color: #DDDDDD;
			font-weight: bold;
			font-family: sans-serif;
		}

		label {
			color: #9BBBDC;
			margin-left: 8px;
		}

		h1 {
			font-family: serif;
		}

		.caption {
			color: #30C698;
		}

		input[type="file"] {
			color: #F29766;
			margin-left: 16px;
		}

		.detail {
			color: #ED77DA;
			font-weight: lighter;
		}

		#myCanvas {
			margin: 0px;
			cursor: move;
		}

		#outbox {
			display: block;
		}

		#background {
			background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAAAAABX3VL4AAAADklEQVQIHWPYXc9QvxsAB2ICdaBJUyUAAAAASUVORK5CYII=);
			background-size: 12px 12px;
			image-rendering: pixelated;
			-ms-interpolation-mode: nearest-neighbor;
			display: inline-block;
      line-height: 1px;
		}


		#controls {
			border: #525252 1px solid;
			display: inline-block;
			line-height: 30px;
			padding: 8px;
			margin-left: 8px;
			position: absolute;
			background-color: #242424;
		}

		.files, .mate-row, .anim-row {
			display:flex;
			align-items: center;
		}

		.padding {
			padding: 0px 60px;
		}

		.thumb {
			width: 32px;
			height: 32px;
		}

		.files label {
			width: 1em;
			margin-right: 48px;
		}

		.anim-row label {
			width: 1em;
			margin-right: 40px;
		}

		.ui-spinner {
			line-height: initial;
			padding: 0px;
			margin-top: 0px;
			margin-bottom: 0px;
		}

		#zoom, #step, #level {
			line-height: initial;
			padding: 0px;
			margin-top: 0px;
			margin-bottom: 0px;
			width: 60px;
		}

		.radio_label {
			margin-left: 0px;
			font-weight: normal;
			white-space: nowrap;
		}

		.loop, #interval, #fcount, #fdiv {
			line-height: initial;
			padding: 0px;
			margin-top: 0px;
			margin-bottom: 0px;
			width: 32px;
		}

		#finfo {
			font-size: small;
			color: gray;
		}

.model_files {
  margin-left: 0.5em;
}
.model_files .desc {
  font-weight: normal;
  margin-left: 1em;
}

.model_files ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.model_files li {
  float: left;
}

.model_files li a {
  display: block;
  color: white;
  font-size: 80%;
  text-align: center;
  padding: 1px 16px;
}

              
            
!

JS

              
                var loader;
var loader2;
var controls;
var camera;
var default_geo;
var timeline;
var loaded = false;
var anim_capture;
var encoder;

$(function () {

	const width = 300 * 1;
	const height = 300 * 1;
	
	// initialize renderer -------------------------------------------------------------------------
	const renderer = new THREE.WebGLRenderer({
		canvas: document.querySelector('#myCanvas'),
		antialias: false,
		alpha: true,
		preserveDrawingBuffer: true
	});
	renderer.setClearColor(0xBBBBBB, 0);
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(width, height);
	
	$("#draw").on("click", function(){
		let img = document.querySelector("#drawed");
		img.src = renderer.domElement.toDataURL( 'image/png' );
		$("#msg").show();
	});

	// initilaize scene
	const scene = new THREE.Scene();

	// initilalize camera
	var viewSize = 253;

	var aspectRatio = width / height;
	camera = new THREE.OrthographicCamera(
		-aspectRatio * viewSize / 2,
		aspectRatio * viewSize / 2,
		viewSize / 2,
		-viewSize / 2,
		-1000,
		1000
	);

	camera.position.set(viewSize, viewSize * Math.tan(deg2rad(39.23)), viewSize); // Isometric position
	camera.lookAt(scene.position);

	camera.zoom = 1.0;
	camera.updateProjectionMatrix();

	controls = new THREE.OrbitControls(camera, renderer.domElement);
	controls.enableKeys = false

	controls.addEventListener("change", function(e){
		$("#zoom").spinner("value", camera.zoom)
	})
	
	$("#reset").on("click", function(){
		controls.reset()
	});

	// Lighting -------------------------------------------------------------------------------------

		const toplight = new THREE.DirectionalLight(0xFFFFFF);
		toplight.intensity = 0.98;
		toplight.position.set(0, 1, 0).normalize();
		toplight.castShadow = true;
		toplight.shadow.mapSize.width = 2048;
		toplight.shadow.mapSize.height = 2048;
		scene.add(toplight);
		const southlight = new THREE.DirectionalLight(0xFFFFFF);
		southlight.intensity = 0.80;
		southlight.position.set(0, 0, 1).normalize();
		southlight.castShadow = true;
		southlight.shadow.mapSize.width = 2048;
		southlight.shadow.mapSize.height = 2048;
		scene.add(southlight);
		const eastlight = new THREE.DirectionalLight(0xFFFFFF);
		eastlight.intensity = 0.608;
		eastlight.position.set(1, 0, 0).normalize();
		eastlight.castShadow = true;
		eastlight.shadow.mapSize.width = 2048;
		eastlight.shadow.mapSize.height = 2048;
		scene.add(eastlight);
		
		const bottomlight = new THREE.DirectionalLight(0xFFFFFF);
		bottomlight.intensity = 0.608;
		bottomlight.position.set(0, -1, 0).normalize();
		bottomlight.castShadow = true;
		bottomlight.shadow.mapSize.width = 2048;
		bottomlight.shadow.mapSize.height = 2048;
		scene.add(bottomlight);
		const northlight = new THREE.DirectionalLight(0xFFFFFF);
		northlight.intensity = 0.80;
		northlight.position.set(0, 0, -1).normalize();
		northlight.castShadow = true;
		northlight.shadow.mapSize.width = 2048;
		northlight.shadow.mapSize.height = 2048;
		scene.add(northlight);
		const westlight = new THREE.DirectionalLight(0xFFFFFF);
		westlight.intensity = 0.608;
		westlight.position.set(-1, 0, 0).normalize();
		westlight.castShadow = true;
		westlight.shadow.mapSize.width = 2048;
		westlight.shadow.mapSize.height = 2048;
		scene.add(westlight);

	LoadModel()

	function LoadModel(file){
		loaded = false;

		if(loader && loader.object){
			if(loader.object) scene.remove(loader.object);
			if(loader2.object) scene.remove(loader2.object);
			loader = undefined;
			loader2 = undefined;
			default_geo = undefined;
			timeline = undefined;
			$("#list").children().remove()
			$("#anim").children().remove()
		}
		
		loader = new BBModelLoader({
			filename: !file ? default_cube_url: undefined, 
			file: file ? file: undefined,
			texture_name: ["#missing", "#missing", "#missing", "#missing", "#missing", "#missing", "#missing", "#missing"],
			shadow: true, 
			side: THREE.FrontSide,
			useAlpha: BBModelLoader.alphaByAlpha,
			opacity: 1.0,
      diagonalStretch: true,
		}).loadBlock(function(object, parts){

			object.scale.set(10, 10, 10);
			
			/// for animate
			loader.meshes.forEach( mesh => {
				mesh.userData.geometry = mesh.geometry.clone()
			})

			loader.materials.forEach( (material, tid) => {
				let faceIdx = loader.meshes[0].geometry.faces.findIndex( face => face.materialIndex == tid)

				let label = capitalize(loader.textures[tid].id ? loader.textures[tid].id: loader.textures[tid].name)
				
				/// create controls
				$("<div/>", {class: "row", idx: faceIdx, tex: tid}).append(
					$("<div/>", {class: "files", idx: faceIdx, tex: tid}).append(
						$("<label/>", {text: label + ":" }),
						$("<canvas/>", {class: "thumb"}),
						$("<input/>", {type: "file", accept: "image/*"}),
						$("<button/>", {class: "hflip", text: "HFlip"}),
						$("<button/>", {class: "vflip", text: "VFlip"}),
						$("<button/>", {class: "rotate", text: "Rotate"}),
					),
					$("<div/>", {class: "mate-row", idx: faceIdx, tex: tid}).append(
						$("<div/>", {class: "padding"}),
						$("<input/>", {class: "shade", name: "mtype"+tid, id: "shade"+tid, type:"checkbox", checked: true}),
						$("<label/>", {text: "Shade", class: "radio_label", for: "shade"+tid}),
						$("<input/>", {class: "dwrite", name: "dwrite"+tid, id: "dwrite"+tid, type:"checkbox", checked: false}),
						$("<label/>", {text: "DepthWrite", class: "radio_label", for: "dwrite"+tid}),
						$("<input/>", {class: "dcheck", name: "dcheck"+tid, id: "dcheck"+tid, type:"checkbox", checked: true}),
						$("<label/>", {text: "DepthTest", class: "radio_label", for: "dcheck"+tid}),
						$("<input/>", {class: "transp", name: "transp"+tid, id: "transp"+tid, type:"checkbox", checked: true}),
						$("<label/>", {text: "Transparent", class: "radio_label", for: "transp"+tid}),
					)
				).appendTo("#list")
				

				$("<div/>", {class: "anim-row row", tex: tid}).append(
					$("<label/>", {text: label + ":" }),
					$("<label/>", {text: "Times:", for: "#loop"}),
					$("<input/>", {class: "loop", value: "1", readonly: true}),
					$("<label/>", {text: "Type:"}),
					$("<input/>", {class: "normal", name: "atype"+tid, id: "normal"+tid, type:"radio", checked: true}),
					$("<label/>", {text: "Normal", class: "radio_label", for: "normal"+tid}),
					$("<input/>", {class: "recipro", name: "atype"+tid, id: "recipro"+tid, type:"radio"}),
					$("<label/>", {text: "Recipro", class: "radio_label", for: "recipro"+tid}),
					$("<input/>", {class: "fade", name: "atype"+tid, id: "fade"+tid, type:"radio"}),
					$("<label/>", {text: "X-Fade", class: "radio_label", for: "fade"+tid}),
				).appendTo("#anim")
			})

			scene.add(object);

			/// create animate event
			timeline = new TimeLineControl()

			{
				/// model for cross-fade animate
				loader2 = new BBModelLoader({
					filename: !file ? default_cube_url: undefined, 
					file: file ? file: undefined,
					texture_name: ["#missing", "#missing", "#missing", "#missing", "#missing", "#missing"],
					shadow: true, 
					side: THREE.FrontSide,
					useAlpha: BBModelLoader.alphaByAlpha,
					opacity: 1.0,
          diagonalStretch: true,
				}).loadBlock(function(object, parts){

					object.scale.set(10, 10, 10);

					/// backup initial faceVertexUVS
					loader2.meshes.forEach( mesh => {
						mesh.userData.geometry = mesh.geometry.clone()
					})

					scene.add(object);

					set_mouse_events()

					loaded = true
					Render(scene);
				})
			}
		});
	}

	function Render(scene) {

		// Run tick -------------------------------------------------------------------------------------

		let begin_sec = new Date().getTime();
		let hilight_lifetime = 0
		let highlit_delta = 1.5

		let time = (new Date()).getTime()
		let anim_lifetime = timeline.total

		tick();

		function tick() {
			if(!loaded || anim_capture) return;
      
			let now = (new Date().getTime() - begin_sec) * 0.001;
			hilight_lifetime = now % highlit_delta / highlit_delta;
      
			$(".row[hover]").each(function(){
				let tex = $(this).attr("tex")
				let v = Math.abs(Math.cos(hilight_lifetime * Math.PI * 2) * 0.5)
				if(loader.materials[tex].emissive){
					loader.materials[tex].emissive.setRGB(v, v, v)
					loader2.materials[tex].emissive.setRGB(v, v, v)
				} else {
					loader.materials[tex].color.setRGB(v, v, v)
					loader2.materials[tex].color.setRGB(v, v, v)
				}
			});

			let nowtime = (new Date()).getTime()
			let interval = $("#interval").spinner("value")

			if(nowtime - time >= interval * 10){
				//let step = $("#step").spinner("value")
				let fcount = $("#fcount").spinner("value")
				let fdiv = $("#fdiv").spinner("value")
				let step = 1 / (fdiv * fcount)

				time = nowtime
				anim_lifetime = anim_lifetime - step > 0 ? anim_lifetime - step: timeline.total
				timeline.process(anim_lifetime)
			}

			// Rendering
			renderer.render(scene, camera);
			requestAnimationFrame(tick);
		}
	}
	

	// APNG writer ----------------------------------------------------------------------------------
	encoder = new APNGencoder(document.querySelector("#myCanvas"));
	encoder.setRepeat(0);
	encoder.setBlend(0);

	$("#create_apng").on("click", function(){

		if(!encoder || !loader || !timeline) return

		let img = document.querySelector("#drawed")
		img.src = ""
		
		let interval = $("#interval").spinner("value") 
		let fcount = $("#fcount").spinner("value")
		let fdiv = $("#fdiv").spinner("value")
		let step = 1 / (fcount * fdiv)

		anim_capture = {}
		anim_capture.delay = step
		anim_capture.max = Math.floor(1 / anim_capture.delay)
		anim_capture.lifetime = timeline.total
		anim_capture.count = 0

		timeline.process(0.00001)
		renderer.compile(scene, camera);

		setTimeout(function(){
			renderer.render(scene, camera);
	
			encoder.setDelay( interval );
			encoder.start();
	
			anim_capture.timer = setTimeout(create_frame , 100)

		}, 100)
		
	});

	function create_frame(){

		clearTimeout(anim_capture.timer)

		if(anim_capture.count <= anim_capture.max){
			if(anim_capture.count > 0){
			  	encoder.addFrame();
				$("#apng_msg").text((anim_capture.count) + "/" + anim_capture.max)
			}

			timeline.process(anim_capture.lifetime)
			renderer.render(scene, camera);

			anim_capture.timer = setTimeout(create_frame , 100)

			anim_capture.count ++;
			anim_capture.lifetime = anim_capture.lifetime - anim_capture.delay > 0 ? anim_capture.lifetime - anim_capture.delay: 0
			
		} else {

			anim_capture = undefined
			encoder.finish();

			var base64Out = bytesToBase64(encoder.stream().bin);
			let apng = document.querySelector("#drawed");
			apng.src = "data:image/png;base64," + base64Out;

			Render(scene);
		}
		
	}
	
	// --------------- controls --------------------
	$("#msg").hide();


	$("#bbmodel").change(function(){
		LoadModel($(this).prop('files')[0]);
	});

	let zoom_changed = function() {
		camera.zoom = $("#zoom").spinner("value");
		camera.updateProjectionMatrix();
	}

	$("#zoom").spinner({
		value: 1,
		min: 0,
		max: 3,
		step: 0.001,
		change: zoom_changed,
		spin: zoom_changed,
		input: zoom_changed,
	})
	$("#zoom").on("input", zoom_changed)
	
	let interval_changed = function(value){
		let total = ($("#fcount").spinner("value") * $("#fdiv").spinner("value") * value) / 100
		$("#finfo").text("(" + ("" + (100 / value)).slice(0, 5) + "fps, " + total + " seconds)")
	}
	
	$("#interval").spinner({
		min: 2,
		max: 100,
		step: 1,
		change: function(){
			interval_changed($("#interval").spinner("value"))
		},
		spin: function(e, o){
			interval_changed(o.value)
		},
		input: function(e, o){
			interval_changed(o.value)
		},
	})
/*
	$("#step").spinner({
		min: 0.0001,
		max: 1,
		step: 0.0001,
	})
	*/
	$("#fcount").spinner({
		min: 1,
		max: 256,
		step: 1,
	})
	$("#fdiv").spinner({
		min: 1,
		max: 256,
		step: 1,
		change: function(){
			interval_changed($("#interval").spinner("value"))
		},
		spin: function(e, o){
      setTimeout(function(){interval_changed($("#interval").spinner("value"))}, 25)
		},
	})
	
	let level_changed = function(value) {
		let val = value != undefined ? value : $("#level").spinner("value");
		loader.object.position.y = val*8;
		loader2.object.position.y = val*8;
	}
	$("#level").spinner({
		min: -16,
		max: 16,
		step: 0.25,
		change: function(){
      level_changed($("#level").spinner("value"))
    },
		spin: function(e, o){
			level_changed(o.value)
    },
		input: function(e, o){
			level_changed(o.value)
    },
	})

	function texture_changed() {
		let file = $(this).prop('files')[0];
		let fileRdr = new FileReader();
		fileRdr.self = this;
		let thumb = $(this).prev();
	
		if(!this.files.length){
			return; //no file selected
		} else {
			if(file.type.match('image.*')){
				fileRdr.onload = function() {
					let tex = $(this.self).parent().attr("tex");
					let imgTmp = new Image();
					imgTmp.src = fileRdr.result;
					imgTmp.onload = function(){

						//change texture
						loader.materials[tex].map.image = this;
						
						loader2.materials[tex].map.image = this;

						// refresh faceVertexUVs
						recalcUV(loader);
						recalcUV(loader2);

						loader.materials[tex].map.needsUpdate = true;
						loader2.materials[tex].map.needsUpdate = true;
							
						// backup initial geometry values
						loader.meshes.forEach( mesh => {
							mesh.userData.geometry = mesh.geometry.clone()
						})
						loader2.meshes.forEach( mesh => {
							mesh.userData.geometry = mesh.geometry.clone()
						})

						//if texture for animate
						if(this.width < this.height && this.height % this.width == 0){
							
							// create animate event
							timeline.addEvent({
								id: tex,
								start: 0,
								interval: 1,
								process: function(ticks){

									let control = this.userData.control
									let loop = control.find(".loop").spinner("value")
									let tick

									if(control.find(".recipro").prop("checked")){
										tick = Math.abs(ticks - 0.5) * 2

									} else {
										tick = (ticks * loop) % 1
									}

									// animate first layer
									loader.meshes.forEach( mesh => {
										flip_animate(mesh, tex, tick)
									})

									if(control.find(".fade").prop("checked")){
										let tick = (ticks * loop) % 1
										let num = Math.floor(tick / (1 / this.userData.texture_count))
										let next = ((num+1) % this.userData.texture_count) / this.userData.texture_count
										let opacity = tick / (1 / this.userData.texture_count) - num
										
										// animate second layer
										loader2.meshes.forEach( mesh => {
											flip_animate(mesh, tex, next)
											mesh.material[tex].opacity = opacity
										})
									} else {
										loader2.meshes.forEach( mesh => {
											mesh.material[tex].opacity = 0
										})
									}
								},
								userData: {
									control: $(".anim-row[tex='" + tex + "']"),
									texture_count: this.height / this.width,
								}
							})
						} else {
							delete timeline[tex]
						}

						//change thumbnail
						thumb[0].setAttribute("width", 32);
						thumb[0].setAttribute("height", 32);
						let ctx = thumb[0].getContext("2d");
						ctx.mozImageSmoothingEnabled = false;
						ctx.webkitImageSmoothingEnabled = false;
						ctx.msImageSmoothingEnabled = false;
						ctx.imageSmoothingEnabled = false;

						ctx.clearRect(0, 0, 32, 32);
						ctx.drawImage(this, 0, 0, 32, 32);

						//auto calc step
						let max = 0
						loader.materials.forEach( material => {
							let img = material.map.image
							max = Math.max(max, img.height / img.width)
						})
						$("#fcount").spinner("value", Math.floor(max))
						$("#fcount").trigger("spinchange")
					}
				}
				fileRdr.readAsDataURL(file);
			} else {
				// not support
			}
		}
	}
	
	function set_mouse_events() {

		$("#list .row").sort( function(a, b){
			return a.getAttribute("tex") - b.getAttribute("tex")
		}).appendTo(list)

		$(".row").mouseenter(function(){
			$(this).attr("hover", "true")
		}).mouseleave(function(){
			$(this).removeAttr("hover")
			let tex = $(this).attr("tex")
			if(loader.materials[tex].emissive){
				loader.materials[tex].emissive.setRGB(0, 0, 0)
				loader2.materials[tex].emissive.setRGB(0, 0, 0)
			} else {
				loader.materials[tex].color.setRGB(1, 1, 1)
				loader2.materials[tex].color.setRGB(1, 1, 1)
			}
		})

		$(".files input[type='file']").change(texture_changed)
		
		let getMinMax = (uvs, x) => {
			let ret = {min: 1, max: 0}
			uvs.forEach( uv => {
				ret.min = Math.min(ret.min, x ? uv.x: uv.y)
				ret.max = Math.max(ret.max, x ? uv.x: uv.y)
			})
			return ret
		}

		// flip holizontal
		$(".hflip").on("click", function(){
			let idx = $(this).parent().attr("idx")*1;
			let flip = (mesh) => {
				let mx = getMinMax(mesh.geometry.faceVertexUvs[0][idx+0], 1)
				mesh.geometry.faceVertexUvs[0][idx+0].forEach( uv => { uv.x = uv.x == mx.min ? mx.max: mx.min } )
				mesh.geometry.faceVertexUvs[0][idx+1].forEach( uv => { uv.x = uv.x == mx.min ? mx.max: mx.min } )
				mx = getMinMax(mesh.userData.geometry.faceVertexUvs[0][idx+0], 1)
				mesh.userData.geometry.faceVertexUvs[0][idx+0].forEach( uv => { uv.x = uv.x == mx.min ? mx.max: mx.min } )
				mesh.userData.geometry.faceVertexUvs[0][idx+1].forEach( uv => { uv.x = uv.x == mx.min ? mx.max: mx.min } )
				mesh.geometry.uvsNeedUpdate = true
			}
			loader.meshes.forEach( mesh => flip(mesh) )
			loader2.meshes.forEach( mesh => flip(mesh) )
		})

		// flip vertical
		$(".vflip").on("click", function(){
			let idx = $(this).parent().attr("idx")*1;
			let flip = mesh => {
				let tex = mesh.geometry.faces[idx].materialIndex
				let mx = getMinMax(mesh.geometry.faceVertexUvs[0][idx+0])
				mesh.geometry.faceVertexUvs[0][idx+0].forEach( uv => { uv.y = uv.y == mx.min ? mx.max: mx.min } )
				mesh.geometry.faceVertexUvs[0][idx+1].forEach( uv => { uv.y = uv.y == mx.min ? mx.max: mx.min } )
				mx = getMinMax(mesh.userData.geometry.faceVertexUvs[0][idx+0])
				mesh.userData.geometry.faceVertexUvs[0][idx+0].forEach( uv => { uv.y = uv.y == mx.min ? mx.max: mx.min } )
				mesh.userData.geometry.faceVertexUvs[0][idx+1].forEach( uv => { uv.y = uv.y == mx.min ? mx.max: mx.min } )
				mesh.geometry.uvsNeedUpdate = true
			}
			loader.meshes.forEach( mesh => flip(mesh) )
			loader2.meshes.forEach( mesh => flip(mesh) )
		})

		// rotate texture
		$(".rotate").on("click", function(){
			let idx = $(this).parent().attr("idx")*1;
			let rot90 = uv => {
				let r = 1.5707963267948966
        let x = uv.x-0.5, y = uv.y - 0.5
				uv.x = x * Math.cos(r) - y * Math.sin(r) + 0.5
				uv.y = x * Math.sin(r) + y * Math.cos(r) + 0.5
			}
			
			let rotate = mesh=> {
				mesh.geometry.faceVertexUvs[0][idx+0].forEach(rot90)
				mesh.geometry.faceVertexUvs[0][idx+1].forEach(rot90)
				
				mesh.userData.geometry.faceVertexUvs[0][idx+0].forEach(rot90)
				mesh.userData.geometry.faceVertexUvs[0][idx+1].forEach(rot90)
				mesh.geometry.uvsNeedUpdate = true
			}
			
			loader.meshes.forEach( mesh=> rotate(mesh) )
			loader2.meshes.forEach( mesh=> rotate(mesh) )
		})

		// shade
		$(".shade").on("click", function(){
			let idx = $(this).parent().attr("tex")*1;
			let checked = $(this).prop("checked")
			let depthCheck = $(this).parent().find(".dcheck").prop("checked")
			let depthWrite = $(this).parent().find(".dwrite").prop("checked")

			let option = {map: loader.materials[idx].map, alphaMap: loader.materials[idx].alphaMap, transparent: true, depthWrite: depthWrite, depthTest: depthCheck}
			loader.meshes[0].material[idx] = checked ? new THREE.MeshLambertMaterial(option) : new THREE.MeshBasicMaterial(option)
			option.map = loader2.materials[idx].map; option.alphaMap = loader2.materials[idx].alphaMap;
			loader2.meshes[0].material[idx] = checked ? new THREE.MeshLambertMaterial(option) : new THREE.MeshBasicMaterial(option)
		})
		
		$(".dwrite").on("click", function(){
			let idx = $(this).parent().attr("tex")*1;
			let checked = $(this).prop("checked")
			try {
				loader.meshes.forEach( mesh=> { mesh.material[idx].depthWrite = checked } )
				loader2.meshes.forEach( mesh=> { mesh.material[idx].depthWrite = checked } )
			} catch{;}
		})
		$(".dcheck").on("click", function(){
			let idx = $(this).parent().attr("tex")*1;
			let checked = $(this).prop("checked")
			try {
				loader.meshes.forEach( mesh=> { mesh.material[idx].depthTest = checked } )
				loader2.meshes.forEach( mesh=> { mesh.material[idx].depthTest = checked } )
			} catch{;}
		})
		$(".transp").on("click", function(){
			let idx = $(this).parent().attr("tex")*1;
			let checked = $(this).prop("checked")
			loader.meshes.forEach( mesh=> {
				mesh.material[idx].transparent = checked
				mesh.material[idx].alphaTest = checked ? 0: 0.9
			})
			loader2.meshes.forEach( mesh=> {
				mesh.material[idx].transparent = checked
				mesh.material[idx].alphaTest = checked ? 0: 0.9
			})
		})

		// loop textbox
		$(".loop").spinner({
			value: 1,
			min: 1,
			max: 10,
			step: 1,
		})
	}

	function control_reset(){
		if(!loader) return

		loader.materials.forEach( (material, i) => {
			$("#shade" + i).prop("checked", material.type == "MeshLambertMaterial")
			$("#dwrite" + i).prop("checked", material.depthWrite )
			$("#dcheck" + i).prop("checked", material.depthCheck )
			$("#transp" + i).prop("checked", material.transparent )
		})
	}
	
	// model rotate
	$("#mrotate").on("click", function(){
		loader.object.rotation.y += deg2rad(45)
		loader2.object.rotation.y += deg2rad(45)
		hilight_box.rotation.y += deg2rad(45)
	})
});

// -----------------------------------------
// misc functions
// -----------------------------------------

// for animated block's texture
function recalcUV(ldr){
	let images = []
	ldr.materials.forEach( material => {
		images.push(material.map.image);
	});
	ldr.recalcUV(images);
}

//
function capitalize(word){
	return word.replace(/^\w/, c => c.toUpperCase());
}

async function jsonLoader(filename){
	return new Promise((resolve, reject) => {
		try {
			let xhr = new XMLHttpRequest();
			xhr.responseType = 'json';
			xhr.open('GET', filename);
			xhr.onload = function(){
				if (xhr.readyState === xhr.DONE) {
					if (xhr.status === 200) {
						try {
							resolve(xhr.response)
						} catch(err) {
							reject(err)
						}
					} else {
						reject(Error(xhr.statusText))
					}
				} else {
					reject(Error(xhr.statusText))
				}
			}
			xhr.onerror = function() {
				reject(Error("Network Error"));
			}
			xhr.send();
		} catch (err){
			reject(err);
		}
	})
}

//------------------------------
// Texture flip methods

function flip_animate (mesh, index, pos) {
	let image = mesh.material[index].map.image
	mesh.geometry.faces.forEach( (face, i) => {
		if(face.materialIndex == index){
			set_vector_pos(mesh.geometry.faceVertexUvs[0][i], mesh.userData.geometry.faceVertexUvs[0][i], image, loader.resolution, pos);
		}
	})
	mesh.geometry.uvsNeedUpdate = true;
}

function set_vector_pos(face, origin_face, texture, resolution, delta)
{
	let calc = (vector, delta) => {
		let xx = vector.x * resolution.width;
		let y = delta * texture.height;
		let yy = (1 - vector.y) * texture.height + (y - y % resolution.height);

		xx = xx > texture.width ? texture.width: xx;
		yy = yy > texture.height ? texture.height: yy;

		return {x: xx / texture.width, y: 1 - yy / texture.height};
	}

	let vexs = [];
	origin_face.forEach((vector, i) => {
		vexs.push(calc(vector, delta))
	});

	face.forEach( (vector, i) => {
		vector.x = vexs[i].x
		vector.y = vexs[i].y
	})

}

//------------------------------
// Timeline Controler Class

var TimeLineControl = function(interval) {
	this.interval = interval;
	this.controls = {};
	this.total = 0;
	return this;
}

TimeLineControl.prototype.addEvent = function( option ) {
	let event = {
		start: option.start,
		interval: option.interval,
		reset: option.reset,
		process: option.process,
		over: option.over,
		userData: option.userData,
		is_over: true,
	}
	this.controls[option.id] = event;
	this.total = this.total < option.start + option.interval ? option.start + option.interval : this.total;
}

TimeLineControl.prototype.process = function( ticks ) {

	let now = (ticks * (this.interval ? this.interval: this.total)) % 1

	Object.values(this.controls).forEach( control => {
		if( now >= control.start && now < control.start + control.interval ){
			if(control.is_over) {
				if(typeof control.reset == "function"){
					control.reset.call(control)
				}
				control.is_over = false
			}
			if(typeof control.process == "function"){
				control.process.call(control, 1 - ((now - control.start) / control.interval) )
			}
		} else
		if(now >= control.start + control.interval && !control.is_over){
			if(typeof control.over == "function"){
				control.over.call(control)
			}
			control.is_over = true
		}
	})
}


function setPos(x, y, z){
	loader.object.position.set(x, y, x)
	loader2.object.position.set(x, y, x)
	hilight_box.position.set(x, y, x)
}

var default_cube_url = "data:application/json;base64,eyJtZXRhIjp7ImZvcm1hdF92ZXJzaW9uIjoiMy4wIiwibW9kZWxfZm9ybWF0IjoiamF2YV9ibG9jayIsImJveF91diI6ZmFsc2V9LCJuYW1lIjoiY3ViZSIsInBhcmVudCI6ImJsb2NrL2Jsb2NrIiwiYW1iaWVudG9jY2x1c2lvbiI6dHJ1ZSwicmVzb2x1dGlvbiI6eyJ3aWR0aCI6MTYsImhlaWdodCI6MTZ9LCJlbGVtZW50cyI6W3sibmFtZSI6ImN1YmUiLCJmcm9tIjpbMCwwLDBdLCJ0byI6WzE2LDE2LDE2XSwiYXV0b3V2IjoyLCJjb2xvciI6MCwib3JpZ2luIjpbOCw4LDhdLCJmYWNlcyI6eyJub3J0aCI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjAsImN1bGxmYWNlIjoibm9ydGgifSwiZWFzdCI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjEsImN1bGxmYWNlIjoiZWFzdCJ9LCJzb3V0aCI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjIsImN1bGxmYWNlIjoic291dGgifSwid2VzdCI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjMsImN1bGxmYWNlIjoid2VzdCJ9LCJ1cCI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjQsImN1bGxmYWNlIjoidXAifSwiZG93biI6eyJ1diI6WzAsMCwxNiwxNl0sInRleHR1cmUiOjUsImN1bGxmYWNlIjoiZG93biJ9fSwidXVpZCI6IjcyOGMzZDJlLTYwNGQtMmZmMC01ZDc3LWU4MWU4Yjk1ODk2MSJ9XSwib3V0bGluZXIiOlsiNzI4YzNkMmUtNjA0ZC0yZmYwLTVkNzctZTgxZThiOTU4OTYxIl0sInRleHR1cmVzIjpbeyJwYXRoIjoiIiwibmFtZSI6IiNub3J0aCIsImZvbGRlciI6IiIsIm5hbWVzcGFjZSI6IiIsImlkIjoibm9ydGgiLCJwYXJ0aWNsZSI6ZmFsc2UsIm1vZGUiOiJsaW5rIiwic2F2ZWQiOnRydWUsInV1aWQiOiI0ZThiMGYzNi1jODgxLTE2Y2ItMTE4Ny1hZWFhNWE5NmQ2MGMifSx7InBhdGgiOiIiLCJuYW1lIjoiI2Vhc3QiLCJmb2xkZXIiOiIiLCJuYW1lc3BhY2UiOiIiLCJpZCI6ImVhc3QiLCJwYXJ0aWNsZSI6ZmFsc2UsIm1vZGUiOiJsaW5rIiwic2F2ZWQiOnRydWUsInV1aWQiOiI2MTA0YzA2OS0zNzExLWUyYmYtNWVkNC04NDUzZDA0NWY2NDIifSx7InBhdGgiOiIiLCJuYW1lIjoiI3NvdXRoIiwiZm9sZGVyIjoiIiwibmFtZXNwYWNlIjoiIiwiaWQiOiJzb3V0aCIsInBhcnRpY2xlIjpmYWxzZSwibW9kZSI6ImxpbmsiLCJzYXZlZCI6dHJ1ZSwidXVpZCI6ImQ3YTk5Yjk5LTg2NzUtZjBkMi1jMWUwLWE1YmNlM2VlMTcxNSJ9LHsicGF0aCI6IiIsIm5hbWUiOiIjd2VzdCIsImZvbGRlciI6IiIsIm5hbWVzcGFjZSI6IiIsImlkIjoid2VzdCIsInBhcnRpY2xlIjpmYWxzZSwibW9kZSI6ImxpbmsiLCJzYXZlZCI6dHJ1ZSwidXVpZCI6Ijc3ZDgzNzFhLTU1NDktYzE1OC1hNGUzLWNiMThlNmY0Yjc5ZiJ9LHsicGF0aCI6IiIsIm5hbWUiOiIjdXAiLCJmb2xkZXIiOiIiLCJuYW1lc3BhY2UiOiIiLCJpZCI6InVwIiwicGFydGljbGUiOmZhbHNlLCJtb2RlIjoibGluayIsInNhdmVkIjp0cnVlLCJ1dWlkIjoiZDM5MGI4MWYtM2Y4NC01ZTZmLTg0NmQtNjBlN2Y2NjYwOTRmIn0seyJwYXRoIjoiIiwibmFtZSI6IiNkb3duIiwiZm9sZGVyIjoiIiwibmFtZXNwYWNlIjoiIiwiaWQiOiJkb3duIiwicGFydGljbGUiOmZhbHNlLCJtb2RlIjoibGluayIsInNhdmVkIjp0cnVlLCJ1dWlkIjoiMWYxMzBiZTQtM2MwOC05NWM0LTBiNzEtNTJkZTllMzZlM2RkIn1dfQ==";

              
            
!
999px

Console