Making a fix scaled timeline in HTML/CSS/JS - javascript

Sorry, this is going to be a little long...
A bit of context
As part of a larger project I'm trying to make a project timeline to include in a webpage used to remote control a music making program (called Reaper for those who are interested). I'm trying to get it to display the current play position, the project markers, and project regions. These are all served straight from the program's API, no problem getting the info. For starters I'm just trying to display the project markers, however I've been pulling my hair out for over a week now trying to get it to work.
Here's a quick screencap from inside the software to illustrate what I'm trying to emulate: Reaper ruler screencap
Normally I would use a progress bar for something like this, or one of the thousands of examples from the web, however I have no way of knowing the length of the project as the software doesn't limit it. As a result I fell back onto using a fixed scale of 10px per bar. Kind of arbitrary, I chose that as it's the optimal for a 5 minute song at 120 bpm. Not too worried about looks for the moment, I just want to get it to work haha.
The problem I have (code is included at the bottom) is that because I'm using absolute positioning for the markers in order to align them all from the left of the screen, they are pulled from the document flow and so I can't wrap them in the parent div. In the end I intend setting the parent div to an 80% width with a scrollbar to see the rest of the markers, so obviously I'm doing it wrong. However I can't seem to find any coded snippets of anything similar to what I'm trying to achieve.
So here is the actual question:
What sort of display/position/float CSS should I be using to do this instead of position: absolute and float: left? If I need JS to do it, then how to I go about it?
Thanks for any help you can bring me, be it actual code or just a nudge in the right direction!
Here's my (relevant) code:
index.html
<html>
<body>
<div id="timeline">
<div id="labels"></div>
<div id="markers"></div>
</div>
</body>
</html>
script.js:
// hardcoded for debugging purposes
// See section below about the API for info about how I get this data
var markers = [
{label: "Start", pos: "20.00000000000000"},
{label: "First", pos: "50.00000000000000"},
{label: "Second", pos: "200.00000000000000"},
{label: "Last", pos: "576.845412000000000"}
];
function draw_markers(marker_list) {
var label_html = "";
var marker_html = "";
$.each(marker_list, function(index, obj) {
var label = obj.label;
var offset = parseFloat(obj.pos) * 7; // obj.pos is mesured in bars, not px
label_html = label_html +
"<span class='label' style='margin-left:"+offset+"px'>" +
label + "</span>";
marker_html = marker_html +
"<span class='marker' style='margin-left:"+offset+"px'>|</span>";
});
document.getElementById("labels").innerHTML = label_html;
document.getElementById("markers").innerHTML = marker_html;
}
draw_markers(markers);
style.css:
html, body {
background: #eeeeee;
}
#timeline {
height: 4em;
width: 100%;
background-color: #9E9E9E;
}
#labels {
border-bottom: 1px solid black;
height: 50%;
width: 100%;
}
#markers {
height: 50%;
width: 100%;
}
.label {
position: absolute;
float: left;
}
.marker {
position: absolute;
float: left;
}
About the API
We're given a bunch of functions that poll the server at regular intervals and parse the (cleartext) responses. A typical response looks something like this:
MARKERLIST_BEGINMARKER_LIST
MARKER \t label \t ID \t position
...
MARKER_LIST_END
TRANSPORT \t playstate \t position_seconds \t isRepeatOn \t position_string \t position_string_beats
...
Using JS I split each line and use a switch statement to figure out what to do with each line. I then build up a global array containing all the marker in the project with just the info I need.

Alternatively you could use <canvas> to draw the timeline from Javascript (this is what I use for Song Switcher's web interface). Also you can get the project length using the GetProjectLength API function (for example, by calling a script to put the length into a temporary extstate then reading it from the web interface).
function Timeline(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.length = 0;
this.markers = [];
}
Timeline.prototype.resize = function() {
this.canvas.width = this.canvas.clientWidth;
this.canvas.height = this.canvas.clientHeight;
this.scale = this.length / this.canvas.width;
}
Timeline.prototype.update = function() {
this.resize();
this.ctx.fillStyle = '#414141';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.textBaseline = 'hanging';
for(var marker of this.markers)
this.drawMarker(marker);
}
Timeline.prototype.drawMarker = function(marker) {
const MARKER_WIDTH = 2;
const FONT_SIZE = 14;
const PADDING = MARKER_WIDTH * 2;
var xpos = this.timeToPx(marker.pos);
this.ctx.strokeStyle = this.ctx.fillStyle = 'red';
this.ctx.lineWidth = MARKER_WIDTH;
this.ctx.beginPath();
this.ctx.moveTo(xpos, 0);
this.ctx.lineTo(xpos, this.canvas.height);
this.ctx.stroke();
if(marker.name.length > 0) {
this.ctx.font = `bold ${FONT_SIZE}px sans-serif`;
var boxWidth = this.ctx.measureText(marker.name).width + PADDING;
this.ctx.fillRect(xpos, 0, boxWidth, FONT_SIZE + PADDING);
this.ctx.fillStyle = 'white';
this.ctx.fillText(marker.name, xpos + MARKER_WIDTH, PADDING);
}
}
Timeline.prototype.timeToPx = function(time) {
return time / this.scale;
}
var timeline = new Timeline(document.getElementById('timeline'));
timeline.length = 30; // TODO: Fetch using GetProjectLength
timeline.markers = [
{pos: 3, name: "Hello"},
{pos: 15, name: "World!"},
{pos: 29, name: ""},
];
timeline.update();
window.addEventListener('resize', timeline.update.bind(timeline));
#timeline {
height: 50px;
line-height: 50px;
image-rendering: pixelated;
width: 100%;
}
<canvas id="timeline"></canvas>

you can use divs as display: inline-block and set their width as percentages of deltas of the overlaping absolute timeline positions:
function draw_markers(marker_list) {
var label_html = "";
var marker_html = "";
var total = null;
var prev = 0;
$.each(marker_list, function(index, obj) {
var delta = parseFloat(obj.pos) - prev;
obj.delta = delta;
prev += parseFloat(obj.pos);
total += delta;
})
$.each(marker_list, function(index, obj) {
var label = obj.label;
var offset = parseFloat(obj.delta) / (total / 100); // obj.pos is mesured in bars, not px
label_html = label_html +
"<div class='label' style='width:"+offset+"%'>" +
label + "</div>";
marker_html = marker_html +
"<div class='marker' style='width:"+offset+"%'>|</div>";
});
document.getElementById("labels").innerHTML = label_html;
document.getElementById("markers").innerHTML = marker_html;
}
draw_markers(markers);
css:
html, body {
background: #eeeeee;
}
#timeline {
height: 4em;
width: 100%;
background-color: #9E9E9E;
}
#labels {
border-bottom: 1px solid black;
height: 50%;
width: 100%;
}
#markers {
height: 50%;
width: 100%;
}
.label {
position: relative;
display: inline-block;
}
.marker {
position: relative;
display: inline-block;
}

Related

How do I move an element with mouse event movement without changing its size in Javascript?

I am developing a custom element representing a window in an operating system. Like a window, it should be moveable/draggable. I can successfully move the window with this code:
moveVertically(movement) {
const boundingClientRect = this.getBoundingClientRect()
this.setTop(boundingClientRect.top + movement)
this.setBottom(boundingClientRect.bottom + movement)
}
moveHorizontally(movement) {
const boundingClientRect = this.getBoundingClientRect()
this.setLeft(boundingClientRect.left + movement)
this.setRight(boundingClientRect.right + movement)
}
The argument movement is mouse event variable movementY and movementX, respectively. The problem is that, if I move the window to quickly, it resizes as well.
How do I move the window without resizing it?
Edit: With this code:
moveVertically(movement) {
const boundingClientRect = this.getBoundingClientRect()
this.style.transform =
`translateY(${boundingClientRect.top + movement}px)`
}
moveHorizontally(movement) {
const boundingClientRect = this.getBoundingClientRect()
this.style.transform =
`translateX(${boundingClientRect.left + movement}px)`
}
the window kind of just moves up and down, when I try to move it. If I comment one of the methods out, it works perfectly, but I can't get both to work at the same time.
I haven't changed my resizing method yet, but that probably doesn't matter.
The reason why your second attempt doesn't work is because you are overwriting the CSS transform. You will need to persist the values on either axis: this can be done by using CSS custom properties for example.
The example below is an adapted example based on your code. The --translate-x and --translate-y are CSS custom properties that can be adjusted individually and they are simply being assigned to the CSS transform property using transform: translate(var(--translate-x, 0), var(--translate-y, 0)); (the fallback value is set to 0):
const stage = document.querySelector('#stage');
const el = document.querySelector('#el');
function moveVertically(movement) {
el.style.setProperty('--translate-y', `${movement}px`);
}
function moveHorizontally(movement) {
el.style.setProperty('--translate-x', `${movement}px`);
}
stage.addEventListener('mousemove', e => {
moveHorizontally(e.clientX);
moveVertically(e.clientY);
});
body {
margin: 0;
padding: 0;
}
#stage {
position: relative;
width: 100vw;
height: 100vh;
}
#el {
position: absolute;
width: 100px;
height: 100px;
background-color: steelblue;
transform: translate(var(--translate-x, 0), var(--translate-y, 0));
}
<div id="stage">
<div id="el"></div>
</div>
Alternatively you can also store the XY coordinates into a variable:
const stage = document.querySelector('#stage');
const el = document.querySelector('#el');
const elCoords = { x: 0, y: 0 };
function moveVertically(movement) {
elCoords.y = movement;
}
function moveHorizontally(movement) {
elCoords.x = movement;
}
stage.addEventListener('mousemove', e => {
moveHorizontally(e.clientX);
moveVertically(e.clientY);
el.style.transform = `translate(${elCoords.x}px, ${elCoords.y}px)`;
});
body {
margin: 0;
padding: 0;
}
#stage {
position: relative;
width: 100vw;
height: 100vh;
}
#el {
position: absolute;
width: 100px;
height: 100px;
background-color: steelblue;
}
<div id="stage">
<div id="el"></div>
</div>

javascript game - joystick issue

Note: I've asked this question again because I was not be able to edit my old question. (No idea if this was a SO bug or a bug with my beta safari.)
So I want to generate a joystick, as it is used in many games. The joystick stands out of a background and a movable billet. The billet may only be moved within the background.
Here you can find both images
let background = new Image()
let stick = new Image()
let enableMaxDistance = false
background.onload = function() {
$(this).css({
position: "absolute",
left: "2%",
bottom: "2%",
width: "30%"
}).appendTo(document.body)
}
stick.onload = function() {
$(this).css({
position: "absolute",
left: "2%",
bottom: "2%",
width: "30%"
}).appendTo(document.body)
let zeroPosition = $(this).offset()
$(this).draggable({
drag: function(e, ui) {
let distance = Math.sqrt(Math.pow(zeroPosition.top - $(this).offset().top, 2) + Math.pow(zeroPosition.left - $(this).offset().left, 2));
if (distance > 60 && enableMaxDistance) {
e.preventDefault();
}
},
scroll: false
})
}
background.src = "https://i.stack.imgur.com/5My6q.png"
stick.src = "https://i.stack.imgur.com/YEoJ4.png"
html, body {
overflow: hidden;
height: 100%;
}
input {
margin-top: 10%;
margin-left: 50%;
}
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<input onclick="enableMaxDistance = !enableMaxDistance " value="toggle maximum distance" type="button"/>
But while implementing this joystick some problems occurred:
My idea was to make the stick maneuverable by using jqueryUI and to calculate its distance to the origin with each drag event. If the distance is too large, the event will be stopped (not executed) using e.preventDefault();. --> If the distance in the frame, the stick is wearable.
The problem is that ...
The stick is no longer draggable after moving out the maximum distance.
The stick should be just be movable inside the bounds without canceling the event so that I have to grab the stick again and again if I'm touching the bounds (going out of the maximum distance).
How to implement a working joystick using jQuery + jQueryUI?
The issue with your logic is that as soon as the drag event is prevented the distance value will be over 60 due to the inherent delays in JS processing time. Therefore the logic in the next drag is immediately cancelled as the distance > 60 check is immediately hit. While it would be possible to fix this, a much better solution would be to not allow the value to ever be greater than the limit you set.
To achieve this I would not recommend using jQueryUI. You can do it quite easily using native methods which give you more direct control of the positioning without having to fight against any built in logic.
It's also slightly more performant, which is vital when dealing with game mechanics; especially when dealing with direct user input which needs to be as responsive as possible.
With that said, you can use modify the basic logic as laid out in Twisty's comment on this question. Then it simply becomes a question of changing the size of the relevant elements, which is a trivial task. Try this:
var $canvas = $('#background');
var $pointer = $('#stick');
var $window = $(window);
var settings = {
width: $canvas.prop('offsetWidth'),
height: $canvas.prop('offsetHeight'),
top: $canvas.prop('offsetTop'),
left: $canvas.prop('offsetLeft')
};
settings.center = [settings.left + settings.width / 2, settings.top + settings.height / 2];
settings.radius = settings.width / 2;
let mousedown = false;
$window.on('mouseup', function() { mousedown = false; });
$pointer.on('mousedown', function() { mousedown = true; });
$pointer.on('mouseup', function() { mousedown = false; });
$pointer.on('dragstart', function(e) { e.preventDefault(); });
$window.on('mousemove', function(e) {
if (!mousedown)
return;
var result = limit(e.clientX, e.clientY);
$pointer.css('left', result.x + 'px');
$pointer.css('top', result.y + 'px');
});
function limit(x, y) {
var dist = distance([x, y], settings.center);
if (dist <= settings.radius) {
return {
x: x,
y: y
};
} else {
x = x - settings.center[0];
y = y - settings.center[1];
var radians = Math.atan2(y, x)
return {
x: Math.cos(radians) * settings.radius + settings.center[0],
y: Math.sin(radians) * settings.radius + settings.center[1]
}
}
}
function distance(dot1, dot2) {
var x1 = dot1[0],
y1 = dot1[1],
x2 = dot2[0],
y2 = dot2[1];
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
html,
body {
height: 100%;
}
#background {
background-image: url('https://i.stack.imgur.com/5My6q.png');
position: absolute;
height: 200px;
width: 200px;
top: 50%;
left: 50%;
margin: -100px 0 0 -100px;
border-radius: 200px;
border: dashed #ccc 1px;
}
#stick {
background: transparent url('https://i.stack.imgur.com/YEoJ4.png') 50% 50%;
position: absolute;
width: 100px;
height: 100px;
border-radius: 100px;
margin: -50px 0 0 -50px;
top: 50%;
left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="background"></div>
<div id="stick"></div>

Draggable square/rectangle that snaps to grid in JQuery/Javascript

I want to create a rectangle on mousedown that drags across a grid and remains there on mouseup, snapping to the gridlines and outputting the coordinates for top left and bottom right of the it's position (x1,x2,y1,y2). Any help on starting to build this would be much appreciated.
I have a 500x500 grid with squares of 10x10 (example - jsFiddle).
Grid Code:
function creategrid(size){
var standardW = Math.floor((500) / size),
standardH = Math.floor((500) / size);
var standard = document.createElement('div');
standard.className = 'grid';
standard.style.width = (standardW * size) + 'px';
standard.style.height = (standardH * size) + 'px';
for (var i = 0; i < standardH; i++) {
for (var p = 0; p < standardW; p++) {
var cell = document.createElement('div');
cell.style.height = (size - 1) + 'px';
cell.style.width = (size - 1) + 'px';
cell.style.position = 'relative'
cell.style.zIndex= '2';
standard.appendChild(cell);
}
}
document.body.appendChild(standard);
}
creategrid(10);
CSS for grid:
.grid {
margin: 0px auto auto;
border: 1px solid #000;
border-width: 0 1px 1px 0;
background-color: #CCC;
}
.grid div {
border: 1px solid #000;
border-width: 1px 0 0 1px;
float: left;
}
#tooltip {
text-align:center;
background:black;
color:white;
padding:3px 0;
width:150px;
position:fixed;
display:none;
white-space:nowrap;
z-index:3;
}
I've found some snapping code through google http://jqueryui.com/draggable/#snap-to but I am literally stuck (I'm a complete beginner at JQuery).
Alternatively if anyone has a better idea of how to do this then that would be more than welcome.
Some background if you want to suggest a different way to do it: This is for a website running off of an SQL server built in python and django. The data it outputs are jSON objects but otherwise I'm just using html, css and javacript/jQuery for the front end. -- Not sure if that info is useful or not.
EDIT added code for mouseover grid coordinates in jQuery
$(window).load(function() {
var tooltip = $('<div id="tooltip">').appendTo('body')[0];
$('.coords').
each(function() {
var pos = $(this).offset(),
top = pos.top,
left = pos.left,
width = $(this).width(),
height = $(this).height();
$(this).
mousemove(function(e) {
var x = ((e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft) - left).toFixed(0),
y = (((e.clientY + document.body.scrollTop + document.documentElement.scrollTop) - top)).toFixed(0);
$(tooltip).text( x + ', ' + y).css({
left: e.clientX + 20,
top: e.clientY + 10
}).show();
}).
mouseleave(function() {
$(tooltip).hide();
});
});
});
If i understood your question correctly, you don't really need jQueryUI for that.
You need to find mouse position snapped to the cell of the grid on mousemove and resize your selection rectangle.
function getMousePos (e) {
return {
'left': Math.floor((e.pageX - gridOffset.left) / cellSpacing) * cellSpacing,
'top': Math.floor((e.pageY - gridOffset.top) / cellSpacing) * cellSpacing
}
}
Here is an example - http://jsfiddle.net/4efTV/
I recommend you to use that plugin, jQuery UI, its really simple to use take a look at this fiddle: http://jsfiddle.net/promatik/hBQxb/
HTML
<div class="snap-box">snap box</div>
Javascript:
$( ".snap-box" ).draggable({ grid: [ 10,10 ] });
CSS:
.snap-box {
width: 50px;
height: 50px;
position: absolute;
z-index: 10;
}

Using Zigfu (kinect) in canvas element of HTML5

I went through the initial tutorial for making a user radar on Zigfu's website. I am having trouble getting this radar to work in the canvas element.
I want to using the drawing methods in canvas, so I don't want it in the container.
Here is my code so far taken directly from the tutorial. Thanks so much for reading!
function loaded() {
var radardiv = document.getElementById('container');
var radar = {
onuserfound: function (user) {
var userdiv = document.createElement('div');
userdiv.className = 'user';
user.radarelement = userdiv;
radardiv.appendChild(user.radarelement);
},
onuserlost: function (user) {
radardiv.removeChild(user.radarelement);
},
ondataupdate: function (zigdata){
for (var userid in zigdata.users){
var user = zigdata.users[userid];
var pos = user.position;
//console.log(pos);
var el = user.radarelement;
var parentElement = el.parentNode;
var zrange = 2000;
var xrange = 700;
var pixelwidth = parentElement.offsetWidth;
var pixelheight = parentElement.offsetHeight;
var heightscale = pixelheight / zrange;
var widthscale = pixelwidth / xrange;
el.style.left = (((pos[0] / xrange) + 0.5) * pixelwidth - (el.offsetWidth / 2)) + "px";
el.style.top = ((pos[2] / zrange) * pixelheight - (el.offsetHeight / 2)) - 150 + "px";
}
}
};
zig.addListener(radar);
}
document.addEventListener('DOMContentLoaded', loaded, false);
<body>
<div id = 'container'></div>
</body>
</html>
<style>
div#container {
width: 800px;
height: 600px;
border: 1px solid black;
overflow: hidden;
}
div.user {
position: relative;
width: 10px;
height: 10px;
background-color: red;
}
It seems you are missing tags around the javascript, as well as some css for the users radar. Also - your 'container' div is missing a >
Try copying the code from the bottom of http://zigfu.com/en/zdk/tutorials/, or - check out http://zigfu.com/en/zdk/recipes/#omercy16 for a cleaner implementation of the users radar.
The radar used in the tutorial makes use of DOM div placement and positioning.
Unfortunately this can't be used inside the canvas element.
There are ways to overlay over the canvas and other workarounds. See: Placing a <div> within a <canvas>
You can also take the data directly from the plugin and draw to the canvas yourself.
Here is a demo using three.js and zigfu to draw the skeleton onto a canvas:
http://blog.kinect.tonkworks.com/post/30569123887/kinect-online-app-javascript-dev-tutorial-1

Overlapping elements - HTML, CSS, and jQuery

I have elements which are overlapping and I would like to prevent this. Here is a picture: http://grab.by/cB7t
Also, here is the CSS for those elements:
.navigationItem {
background: #808080;
-webkit-border-radius: 360px;
padding: 1.0em;
text-decoration: none;
color: #fff;
position: absolute;
-webkit-box-shadow: 0px 2px 5px #909090;
font-weight: bold;
text-shadow: 1px 1px 2px #707070;
font-size: 1.0em;
}
And here they are in the HTML:
play
register
our blog
contact us
about us
our rules`
As you can see, I am using them as simple styled links using the HTML a tag. The reason that their positions are absolute is because I am moving them using jQuery:
function moveAll() {
for(var i = 0; i < AMOUNT; i++) {
var random = Math.random() * 500;
$("#nav" + i).animate({"left": random + i + "px"}, "slow");
$("#nav" + i).animate({"top": random + i + "px"}, "slow");
}
}
When they move, though, they sometimes overlap which is annoying. How can I prevent them from overlapping? Thank you for your efforts.
Removing position:absolute would render them side by side.
JSFiddle
But if the whole point is to scatter them around randomly, then you will have to keep track of positioned elements and take that into account when calculating their position. You should save each link's position and calculate every next link's position according to previous already positioned links. There's simply no other way when you want random positions and non overlapping.
Final non-overlapping solution
This is a working example of non-overlapping functionality. If you'd want your links to not even touch, you should change < to <= and > to >= in the if statement condition.
Relevant code
var positions = [];
$(".navigationItem").each(function(){
var ctx = $(this);
var dim = {
width: ctx.outerWidth(),
height: ctx.outerHeight()
};
var success = false;
// repeat positioning until free space is found
while (!success)
{
dim.left = parseInt(Math.random() * 300);
dim.top = parseInt(Math.random() * 300);
var success = true;
// check overlapping with all previously positioned links
$.each(positions, function(){
if (dim.left < this.left + this.width &&
dim.left + dim.width > this.left &&
dim.top < this.top + this.height &&
dim.top + dim.height > this.top)
{
success = false;
}
});
}
positions.push(dim);
ctx.animate({
left: dim.left,
top: dim.top
}, "slow");
});
You can change the position value to relative.
See my example : http://jsfiddle.net/NmmX6/2/
I changed your loop so that it isn't id dependent :
function moveAll() {
$('.navigationItem').each(function(i,e){
var rdm = Math.random() * 500;
$(e).animate({"left":rdm + "px"}, "slow");
$(e).animate({"top": rdm + "px"}, "slow");
});
}
I tested it and did not find one case where it actually overlaps, but check it out.

Categories

Resources