297 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!doctype html>
 | 
						|
<!-- prototyped this in ~24 hours to help so i don't have to manually enter lists of coordinates for gameboy sprite animations -->
 | 
						|
<html>
 | 
						|
<head>
 | 
						|
	<title>curve editor for gameboy animation</title>
 | 
						|
	<style>
 | 
						|
		body {
 | 
						|
			display: flex;
 | 
						|
			flex-direction: row;
 | 
						|
			gap: 1em;
 | 
						|
		}
 | 
						|
		#controls {
 | 
						|
			display: flex;
 | 
						|
			flex-direction: column;
 | 
						|
			gap: 1em;
 | 
						|
		}
 | 
						|
	</style>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
	<svg 
 | 
						|
		viewBox="0 0 176 176" 
 | 
						|
		style="border: 1px solid black; display: block; max-height: 700px"
 | 
						|
		xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
						|
		<defs>
 | 
						|
			<g id="point">
 | 
						|
				<rect id="sprite" x="0" y="0" width="8" height="8" stroke="none" fill="gray" fill-opacity="25%"/>
 | 
						|
				<circle cx="0.5" cy="0.5" r="2" fill="black" fill-opacity="25%"/>
 | 
						|
				<circle cx="0.5" cy="0.5" r="0.5" fill="black" fill-opacity="100%"/>
 | 
						|
			</g>
 | 
						|
	    <pattern id="smallGrid" width="1" height="1" patternUnits="userSpaceOnUse">
 | 
						|
	      <path d="M 1 0 L 0 0 0 1" fill="none" stroke="darkseagreen" stroke-width="0.25"/>
 | 
						|
	    </pattern>
 | 
						|
	    <pattern id="grid" width="8" height="8" patternUnits="userSpaceOnUse">
 | 
						|
	      <rect width="8" height="8" fill="url(#smallGrid)"/>
 | 
						|
	      <path d="M 8 0 L 0 0 0 8" fill="none" stroke="darkseagreen" stroke-width="0.5"/>
 | 
						|
	    </pattern>
 | 
						|
	  </defs>
 | 
						|
	      
 | 
						|
	  <image 
 | 
						|
	  	id="background-container"
 | 
						|
	  	width="160"
 | 
						|
	  	height="144"
 | 
						|
	  	x="8" y="16"
 | 
						|
	  	opacity="50%"
 | 
						|
	  	xlink:href="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />
 | 
						|
		<rect width="100%" height="100%" fill="url(#grid)" />
 | 
						|
		<rect x="8" y="16" width="160" height="144" style="fill: none; stroke: darkseagreen; stroke-width: 0.5"/>
 | 
						|
		<g id="lines">
 | 
						|
		</g>
 | 
						|
		<g id="sequence">
 | 
						|
		</g>
 | 
						|
	</svg>
 | 
						|
	<div id="controls">
 | 
						|
		<label for="background-selector">file for background</label>
 | 
						|
		<input type="file" id="background-selector" name="background-selector"/>
 | 
						|
		<fieldset>
 | 
						|
			<legend>control mode</legend>
 | 
						|
			<div>
 | 
						|
				<input type="radio" name="mode" id="add" value="add" checked/>
 | 
						|
				<label for="add"><strong>a</strong>dd points</label>
 | 
						|
			</div>
 | 
						|
			<div>
 | 
						|
				<input type="radio" name="mode" id="delete" value="delete"/>
 | 
						|
				<label for="delete"><strong>d</strong>elete points</label>
 | 
						|
			</div>
 | 
						|
			<div>
 | 
						|
				<input type="radio" name="mode" id="move" value="move"/>
 | 
						|
				<label for="move"><strong>g</strong>rab and move points</label>
 | 
						|
			</div>
 | 
						|
		</fieldset>
 | 
						|
		<div>
 | 
						|
			<label for="sprite_width">width in 8x8 sprites:</label>
 | 
						|
			<input type="number" 
 | 
						|
				id="sprite_width" 
 | 
						|
				name="sprite_width" 
 | 
						|
				min="1" max="16" step="1" value="1" 
 | 
						|
				onchange="sprite_dims_change()"/>
 | 
						|
		</div>
 | 
						|
		<div>
 | 
						|
			<label for="sprite_height">height in 8x8 sprites:</label>
 | 
						|
			<input type="number" 
 | 
						|
				id="sprite_height" 
 | 
						|
				name="sprite_height" 
 | 
						|
				min="1" max="16" step="1" value="1"
 | 
						|
				onchange="sprite_dims_change()"/>
 | 
						|
		</div>
 | 
						|
		<p>output is a length-prefixed list of y,x pairs.</p>
 | 
						|
		<textarea id="output">db 0,</textarea>
 | 
						|
	</div>
 | 
						|
</body>
 | 
						|
<script>
 | 
						|
	var svg = null;
 | 
						|
	var svg_group = null;
 | 
						|
	var text_output = null;
 | 
						|
	var moving_point_index = -1;
 | 
						|
	var the_list = [];
 | 
						|
	var current_viewport = {center: {x: 0, y: 0}, radius: {x: 176, y: 176}};
 | 
						|
	var max_viewport = {center: {x: 0, y: 0}, radius: {x: 176, y:176}};
 | 
						|
 | 
						|
	function length(dx, dy) {return Math.sqrt(dx*dx + dy*dy);}
 | 
						|
 | 
						|
	const clicksvg = (event) => {
 | 
						|
		const mode = document.querySelector('input[name=mode]:checked').value; // either 'add', 'delete', or 'move'
 | 
						|
		const {x, y} = dom_to_svg_coords(svg, event.clientX, event.clientY);
 | 
						|
 | 
						|
		if (mode == "add") { 
 | 
						|
			the_list.push({x: Number((x-0.5).toFixed()),y: Number((y-0.5).toFixed())});
 | 
						|
			console.log(Number(x.toFixed()))
 | 
						|
			regenerate_svg_list();
 | 
						|
			regenerate_text_list();
 | 
						|
		}
 | 
						|
 | 
						|
		if (mode == "delete") {
 | 
						|
			let closest = -1;
 | 
						|
			let distance = 3;
 | 
						|
			the_list.forEach((point, index) => {
 | 
						|
				let new_distance = length(point.x-x, point.y-y);
 | 
						|
				if (new_distance < distance) {
 | 
						|
					distance = new_distance;
 | 
						|
					closest = index;
 | 
						|
				}
 | 
						|
			});
 | 
						|
 | 
						|
			if (closest != -1) {
 | 
						|
				the_list.splice(closest, 1);
 | 
						|
			}
 | 
						|
 | 
						|
			regenerate_svg_list();
 | 
						|
			regenerate_text_list();
 | 
						|
		}
 | 
						|
 | 
						|
		if (mode == "move" && moving_point_index != -1) {
 | 
						|
			const elem = document.querySelector(`g#sequence use:nth-child(${moving_point_index+1})`);
 | 
						|
 | 
						|
			let newX = Number(Number(elem.getAttributeNS(null, "x")).toFixed());
 | 
						|
			let newY = Number(Number(elem.getAttributeNS(null, "y")).toFixed());
 | 
						|
			the_list[moving_point_index] = {x: newX, y: newY};
 | 
						|
			moving_point_index = -1;
 | 
						|
			
 | 
						|
			regenerate_svg_list();
 | 
						|
			regenerate_text_list();
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const mousemovesvg = (event) => {
 | 
						|
		const mode = document.querySelector('input[name=mode]:checked').value; // either 'add', 'delete', or 'move'
 | 
						|
		const {x, y} = dom_to_svg_coords(svg, event.clientX, event.clientY);
 | 
						|
 | 
						|
		if (mode == 'move' && moving_point_index != -1) {
 | 
						|
			const previous_mouse_location = {x: event.clientX - event.movementX, y: event.clientY - event.movementY};
 | 
						|
			const prev_location_in_gbspace = dom_to_svg_coords(svg, previous_mouse_location.x, previous_mouse_location.y);
 | 
						|
			const gb_movement = {x: x - prev_location_in_gbspace.x, y: y - prev_location_in_gbspace.y};
 | 
						|
 | 
						|
			const point = the_list[moving_point_index];
 | 
						|
			const elem = document.querySelector(`g#sequence use:nth-child(${moving_point_index+1})`);
 | 
						|
 | 
						|
			let newX = Number(elem.getAttributeNS(null, "x")) + gb_movement.x;
 | 
						|
			let newY = Number(elem.getAttributeNS(null, 'y')) + gb_movement.y;
 | 
						|
			elem.setAttributeNS(null, "x", newX);
 | 
						|
			elem.setAttributeNS(null, "y", newY);
 | 
						|
 | 
						|
			const lineTo = document.querySelector(`g#lines line:nth-child(${moving_point_index})`);
 | 
						|
			if (lineTo) {
 | 
						|
				lineTo.setAttributeNS(null, "x2", newX+0.5);
 | 
						|
				lineTo.setAttributeNS(null, "y2", newY+0.5);
 | 
						|
			}
 | 
						|
 | 
						|
			const lineFrom = document.querySelector(`g#lines line:nth-child(${moving_point_index+1})`);
 | 
						|
			if (lineFrom) {
 | 
						|
				lineFrom.setAttributeNS(null, "x1", newX+0.5);
 | 
						|
				lineFrom.setAttributeNS(null, "y1", newY+0.5);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const mousedownsvg = (event) => { 
 | 
						|
		const mode = document.querySelector('input[name=mode]:checked').value; // either 'add', 'delete', or 'move'
 | 
						|
		const {x, y} = dom_to_svg_coords(svg, event.clientX, event.clientY);
 | 
						|
		let closest = -1;
 | 
						|
		let distance = 3;
 | 
						|
		the_list.forEach((point, index) => {
 | 
						|
			let new_distance = length(point.x-x, point.y-y);
 | 
						|
			if (new_distance < distance) {
 | 
						|
				distance = new_distance;
 | 
						|
				closest = index;
 | 
						|
			}
 | 
						|
		});
 | 
						|
 | 
						|
		if (mode == 'move') {
 | 
						|
			moving_point_index = closest;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const changetextarea = (event) => {
 | 
						|
		let raw = event.target.value;
 | 
						|
		let stripped = raw.replace(/db [0-9]+,/, "");
 | 
						|
		let pairs = stripped.matchAll(/ ?[0-9]+, ?[0-9]+/g);
 | 
						|
		let splits = pairs.map((x) => x[0].split(/,/));
 | 
						|
		let points = splits.map(([a, b]) => { return {y: Number(a), x: Number(b)}; } );
 | 
						|
 | 
						|
		the_list = [... points];
 | 
						|
 | 
						|
		regenerate_svg_list();
 | 
						|
		regenerate_text_list();
 | 
						|
	}
 | 
						|
 | 
						|
	const sprite_dims_change = (event) => {
 | 
						|
		const width = document.querySelector('input#sprite_width').value;
 | 
						|
		const height = document.querySelector('input#sprite_height').value;
 | 
						|
 | 
						|
		const rect = document.querySelector('g#point rect#sprite')
 | 
						|
		rect.setAttributeNS(null, "width", width*8);
 | 
						|
		rect.setAttributeNS(null, "height", height*8);
 | 
						|
	}
 | 
						|
 | 
						|
	
 | 
						|
 | 
						|
	function regenerate_svg_list() {
 | 
						|
		svg_group.textContent = '';
 | 
						|
		for (const {x, y} of the_list) {
 | 
						|
			const newcircle = document.createElementNS("http://www.w3.org/2000/svg", "use");
 | 
						|
			newcircle.setAttributeNS(null, "href", "#point");
 | 
						|
			newcircle.setAttributeNS(null, "x", x);
 | 
						|
			newcircle.setAttributeNS(null, "y", y);
 | 
						|
			svg_group.appendChild(newcircle);
 | 
						|
		}
 | 
						|
		document.querySelector('g#lines').textContent = '';
 | 
						|
		for (const [second, first] of the_list.slice(1, the_list.length).map((e,i) => [e, the_list[i]])) {
 | 
						|
			const newline = document.createElementNS("http://www.w3.org/2000/svg", "line");
 | 
						|
			newline.setAttributeNS(null, "x1", first.x+0.5);
 | 
						|
			newline.setAttributeNS(null, "y1", first.y+0.5);
 | 
						|
			newline.setAttributeNS(null, "x2", second.x+0.5);
 | 
						|
			newline.setAttributeNS(null, "y2", second.y+0.5);
 | 
						|
			newline.setAttributeNS(null, "stroke", "black");
 | 
						|
			newline.setAttributeNS(null, "stroke-width", "0.25");
 | 
						|
			newline.setAttributeNS(null, "stroke-opacity", "50%");
 | 
						|
			document.querySelector('g#lines').appendChild(newline);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	function regenerate_text_list() {
 | 
						|
		s = `db ${the_list.length}, `;
 | 
						|
		for (const {x, y} of the_list) {
 | 
						|
			s += `${y}, ${x}, `;
 | 
						|
		}
 | 
						|
		text_output.value = s;
 | 
						|
	}
 | 
						|
 | 
						|
	function dom_to_svg_coords(s, clientX, clientY) {
 | 
						|
		// thanks to https://www.sitepoint.com/how-to-translate-from-dom-to-svg-coordinates-and-back-again/
 | 
						|
		const pt = s.createSVGPoint();
 | 
						|
		pt.x = clientX;
 | 
						|
		pt.y = clientY;
 | 
						|
		const svgP = pt.matrixTransform( s.getScreenCTM().inverse() );
 | 
						|
		return svgP;
 | 
						|
	}
 | 
						|
 | 
						|
	window.onload = () => {
 | 
						|
		svg = document.querySelector('svg');
 | 
						|
		svg.onclick = clicksvg;
 | 
						|
		svg.onmousemove = mousemovesvg;
 | 
						|
		svg.onmousedown = mousedownsvg;
 | 
						|
		svg_group = document.querySelector('g#sequence');
 | 
						|
		text_output = document.querySelector('textarea#output');
 | 
						|
		text_output.onchange = changetextarea;
 | 
						|
		sprite_dims_change();
 | 
						|
 | 
						|
		let filePicker = document.querySelector('input#background-selector');
 | 
						|
		filePicker.onchange = (event) => {
 | 
						|
			var file = event.target.files[0];
 | 
						|
 | 
						|
			// setting up the reader
 | 
						|
		  var reader = new FileReader();
 | 
						|
		  reader.readAsDataURL(file,'UTF-8');
 | 
						|
 | 
						|
		  // here we tell the reader what to do when it's done reading...
 | 
						|
		  reader.onload = readerEvent => {
 | 
						|
		    var content = readerEvent.target.result; // this is the content!
 | 
						|
		    document
 | 
						|
		    	.querySelector('image#background-container')
 | 
						|
		    	.setAttributeNS("http://www.w3.org/1999/xlink", "href", content);
 | 
						|
		  }
 | 
						|
		}
 | 
						|
		if (filePicker.files[0]) {
 | 
						|
			filePicker.onchange( {target: filePicker} );
 | 
						|
		}
 | 
						|
 | 
						|
		document.onkeyup = (event) => {
 | 
						|
			const add_radio = document.querySelector('input[name=mode]#add'); // either 'add', 'delete', or 'move'
 | 
						|
			const delete_radio = document.querySelector('input[name=mode]#delete');
 | 
						|
			const move_radio = document.querySelector('input[name=mode]#move');
 | 
						|
 | 
						|
			if (event.key == 'a') { add_radio.checked = true; }
 | 
						|
			if (event.key == 'd') { delete_radio.checked = true; }
 | 
						|
			if (event.key == 'g') { move_radio.checked = true; }
 | 
						|
		}
 | 
						|
 | 
						|
 | 
						|
	}
 | 
						|
</script>
 | 
						|
</html> |