I'm creating my own CSS tilemap. The tilemap is 5x5, and will be 1000x1000px in size.
I would like to add it to a 500x500px canvas element so that only 1/2 the map is shown. I have a camera function that center the player to the canvas and move the tilemap relative to the player.
Goal: Put CSS Tilemap as a background in the HTML5 canvas (even though CSS tilemap is larger than canvas, there should be no overlap)
Given map array
var mapArray = [
[0, 0, 0, 0 ,0],
[0, 1, 0, 0 ,0],
[0, 0, 0, 0 ,0],
[0, 0, 0, 0 ,0],
[0, 0, 1, 1 ,0]
];
I draw my map as such:
function drawMap() {
for (var i = 0; i < mapArray.length; i++) {
for (var j = 0; j < mapArray[i].length; j++) {
if (parseInt(mapArray[i][j]) == 0) {
$('#mapContainer').append('<div class="grass"></div>');
}
if (parseInt(mapArray[i][j]) == 1) {
$('#mapContainer').append('<div class="dirt"></div>');
}
}
}
}
CSS: I've tried appending the element directly to the HTML, and used z-index: -1 to perhaps push the tilemap to the background, but that didn't work.
#mapContainer {
height: 1000px;
width: 1000px;
position: relative;
outline: 1px solid black;
z-index: -1;
}
.grass {
height: 200px;
width: 200px;
position: relative;
float: left;
background-color: green;
z-index: -1;
}
.dirt {
height: 200px;
width: 200px;
position: relative;
float: left;
background-color: tan;
z-index: -1;
}
How can I add the #mapContainer element to the canvas? I am using createJS, so I tried doing this:
function createWorld() {
world = new createjs.Container();
var htmlElement = document.createElement('div');
htmlElement.id = 'mapContainer';
var domElement = new createjs.DOMElement(htmlElement);
world.addChild(domElement);
stage.addChild(world);
}
Any thoughts?
It sounds like you are spawning a div to contain a background image for every tile, you want the browser to render that behind your canvas (with the canvas translucent)?
I've done the divs-for-tiles before and I recommend against it. However, you should be able to achieve what your asking.
the canvas has the style background: transparent; or <canvas-element>.style.background = 'transparent';.
the canvas should be on top of the texture divs. I would recommend just making sure the canvas element occurs after the texture elements in the DOM tree / HTML, But if thats not easy z-index styles can take care of that too. I've never used negative z-indices's but they should work.
I would recommend doing the tile set rendering within the canvas. I've scaffolded soemthing like that together a long time ago. Your welcome to reuse any code from this old project: http://thorsummoner.github.io/old-html-tabletop-test/ source: https://github.com/thorsummoner/old-html-tabletop-test/blob/master/index.html
Related
I am trying to make an animation system for my three.js project. I have a json file for the information. I was able to make the animation play. But, at the moment they need to be somewhere, they move to that location, instead of slowly moving to that location over time. The json file tells the program where the specific object needs to be at a certain location. For example:
Json File:
{
"right": {
"position": {
"0.0": [0, 0, 0],
"0.25": [0, 1, 0],
"0.5": [1, 1, 0]
}
}
Json files tells you position, at what second, then the positions.
Code:
for (const [key, value] of Object.entries(json.position.right))
if(seconds === json.position.right[key]) {
obj.position.x = json.right.position[key][0];
obj.position.y = json.right.position[key][1];
obj.position.z = json.right.position[key][2];
}
}
In the code, I loop through the json file's right cube position (which tells when the position changes happen). If the seconds match, it moves to that position.
How would I be able to get the movement inbetween the keyframes for the object?
Here is the example: https://mixed-polyester-a.glitch.me/
Here is all the code: https://glitch.com/edit/#!/mixed-polyester-a
I used Blockbench to export models as .OBJ files, materials as .MTL files, and animations as .JSON files.
Sorry if it sounds confusing, didn't really know how to explain it. Any help would be highly appreciated.
Three.js has a method called MathUtils.lerp() that takes in a starting position, an ending position, and an interpolation value between [0, 1]. You could use this to tween your object's current position to its target destination on each frame, as demonstrated in the example below:
const container = document.getElementById("container");
const ball = document.getElementById("ball");
// Set current positions
let ballPosX = 0;
let ballPosY = 0;
// Set target positions
let ballTargetX = 0;
let ballTargetY = 0;
// Update target positions on click
function onClick(event) {
ballTargetX = event.layerX;
ballTargetY = event.layerY;
//console.log(event);
}
function update() {
// Interpolate current position towards targets
ballPosX = THREE.MathUtils.lerp(ballPosX, ballTargetX, 0.1);
ballPosY = THREE.MathUtils.lerp(ballPosY, ballTargetY, 0.1);
// Apply current position to our object
ball.style.left = ballPosX + "px";
ball.style.top = ballPosY + "px";
requestAnimationFrame(update);
}
container.addEventListener("click", onClick);
update();
#container {
width: 300px;
height: 300px;
background: #ddd;
}
#ball {
width: 10px;
height: 10px;
position: absolute;
top: 0;
left: 0;
background: #f90;
border-radius: 10px;
margin-top: -5px;
margin-left: -5px;
}
<div id="container">
<div id="ball"></div>
</div>
<script src="https://threejs.org/build/three.js"></script>
Update:
To create a linear timeline with keyframes, I've used gsap.to() with the keyframes parameter to feed all the positions to the timeline. See here and look up "keyframes" for more details. You can see it in action in the code demo below, you'll need to iterate through your JSON to feed that data to GSAP on your own, though. Good luck!
// Set position vector
const ballPos = {x: 0, y: 0};
const positions = {
"0.0": [0, 0],
"0.25": [0, 100],
"0.5": [100, 100],
"0.75": [100, 0],
"1.0": [0, 0],
}
const timeline = gsap.to(ballPos, {keyframes: [
{x: positions["0.0"][0], y: positions["0.0"][1], duration: 0.0},
{x: positions["0.25"][0], y: positions["0.25"][1], duration: 0.25},
{x: positions["0.5"][0], y: positions["0.5"][1], duration: 0.25},
{x: positions["0.75"][0], y: positions["0.75"][1], duration: 0.25},
{x: positions["1.0"][0], y: positions["1.0"][1], duration: 0.25},
]});
const container = document.getElementById("container");
const ball = document.getElementById("ball");
let timelineTime = 0;
function update() {
timelineTime += 0.001;
timelineTime %= 1;
timeline.seek(timelineTime);
// Apply current position to our object
ball.style.left = ballPos.x + "px";
ball.style.top = ballPos.y + "px";
requestAnimationFrame(update);
}
update();
#container {
width: 300px;
height: 300px;
background: #ddd;
}
#ball {
width: 10px;
height: 10px;
position: absolute;
top: 0;
left: 0;
background: #f90;
border-radius: 10px;
margin-top: -5px;
margin-left: -5px;
}
<div id="container">
<div id="ball"></div>
</div>
<script src="https://threejs.org/build/three.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.1/gsap.min.js"></script>
I want to make a background that adds and removes square divs based on the size of the container, which is affected by resizing the window.
To add the divs, I followed the examples on this post here. But, resizing the screen continues to multiply the number of divs. Is there a way to constrain the amount of squares to the size of the container, or to remove the overflow?
(I don't want to simply css overflow:hidden because that doesn't solve the problem of a billion divs being multiplied.) And I'm an absolute javascript newbie, so bear with me!
let contain = document.getElementById("squareContain");
let width = contain.offsetWidth;
let height = contain.offsetHeight;
var containerArea = width * height;
var canAdd = Math.floor(containerArea/1600); //For 40px x 40px squares
function multiplyNode(node, count, deep) {
for (var i = 0, copy; i < count - 1; i++) {
copy = node.cloneNode(deep);
node.parentNode.insertBefore(copy, node);
}
}
$(window).on("resize", function(){
multiplyNode(document.querySelector('.square'), canAdd, false);
}).resize();
Edit jsfiddle
Currently you only calculate how many squares fit once, but you need recalculate each time window size changed:
let contain = document.getElementById("squareContain");
function canAdd()
{
let square = contain.children[0],
cWidth = contain.offsetWidth,
cHeight = contain.offsetHeight,
sWidth = square.offsetWidth,
sHeight = square.offsetHeight;
return Math.floor(cWidth / sWidth) * Math.floor(cHeight / sHeight);
}
function multiplyNode(node, count, deep) {
if (contain.children.length == count)
return;
if (contain.children.length < count)
{
for (var i = 0, copy; i < count - 1; i++) {
copy = node.cloneNode(deep);
node.parentNode.insertBefore(copy, node);
}
}
else
{
while(contain.children.length > count)
{
contain.removeChild(contain.children[contain.children.length -1]);
}
}
}
$(window).on("resize", function(){
multiplyNode(contain.querySelector('.square'), canAdd(), false);
}).resize();
.square_container{
background-color: #ccc;
position: fixed;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-content: start;
flex-wrap: wrap;
margin: 0 auto;
padding: 0 auto;
}
.square{
width: 40px;
height: 40px;
background-color: blue;
border: 1px solid red;
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="squareContain" class="square_container">
<div class="square"></div><!--...etc...-->
</div>
What I'm trying to do (which I've been able to accomplish, but with poor performances) is to apply a sort of grid over the canvas, in order to be able to take inputs from the user about the origin point position. Once the input is received, the "draw" coordinates are provided via keyboard.
What I've managed to do in these days, was to calculate the width and height of the canvas, then divide it by the area of a standard 20x20 square (speaking in px). In this way I can loop on the result and create n squares, that I will render in display flex inside the grid element. Then this grid element is applied "over" the canvas.
Everything works, but there's a lot of divs going around, and if the user choses to shrink the div to let's say 10x10, then, that would have a great impact over the performances... So I'm trying to find out a lighter way to do this...
I've thought about using HR elements inside two divs that would be applied over the canvas. One div displays elements in column, and another in row. In this way I should obtain the grid, but what about the snap? How could I detect the intersection on the two HR elements and use that exact spot as position?
The reason of why I cannot directly draw the grid on the canvas is because this should remain as 'pure' as possible. Containing only the final draw of the user.
Here's the 'non optimized' code:
I'm using Angular 5 as framework.
<div class="draw-zone" #drawZone>
<div class="grid" #grid [ngClass]="{'activated': activateDrawZones}">
<div *ngFor="let block of gridBlocks" class="grid-block" [ngClass]="{'show': showGrid, 'ten-x-ten': blockSize === 10, 'twe-x-twe': blockSize === 20, 'thr-x-thr': blockSize === 30, 'fou-x-fou': blockSize === 40}"
#gridBlock (click)="draw($event, gridBlock)"></div>
</div>
<canvas #canvas [height]="canvasSize.y" [width]="canvasSize.x"></canvas>
</div>
The scss:
.draw-zone{
flex-grow: 2;
height: 100%;
position: relative;
canvas{
z-index: 10;
}
.grid{
top: 0;
left: 0;
z-index: 11;
width: 100%;
display: flex;
flex-wrap: wrap;
overflow: hidden;
position: absolute;
margin-left: -.1rem;
border-radius: .5rem;
align-content: stretch;
border: 1px solid transparent;
&.activated{
border-color: #3f51b5;
}
.grid-block{
opacity: 0;
border-right: 1px solid #3f51b5;
border-bottom: 1px solid #3f51b5;
&.show{
opacity: .1;
}
&:hover{
opacity: 1;
border-radius: 50%;
background-color: #3f51b5;
transform: scale(1.2);
}
&.ten-x-ten{
width: 10px;
height: 10px;
}
&.twe-x-twe{
width: 20px;
height: 20px;
}
&.thr-x-thr{
width: 30px;
height: 30px;
}
&.fou-x-fou{
width: 40px;
height: 40px;
}
}
}
And the component method to cal:
private calculateGrid() {
this.activateDrawZones = false;
this.canvasSize.x = this._drawZone.nativeElement.clientWidth;
this.canvasSize.y = this._drawZone.nativeElement.clientHeight;
const blocksCount = (this.canvasSize.x * this.canvasSize.y) / (this.blockSize * this.blockSize);
this.gridBlocks = [];
for (let i = 0; i < blocksCount; i++) {
this.gridBlocks.push({ size: this.blockSize });
}
this.activateDrawZones = true;
}
And the method that actually draws:
public draw(e: MouseEvent, block: HTMLDivElement, returnOnFail?: boolean) {
const x = block.offsetLeft + (this.blockSize / 2);
const y = block.offsetTop + (this.blockSize / 2);
if (this.firstClick) {
this.ctx.beginPath();
this.ctx.moveTo(x, y);
this.setCrosshair(x, y);
this.firstClick = false;
this.addPathToDrawSequence(x, y);
return;
}
if (this.isNotOnTheSameAxisAsTheLastInsert(x, y)) {
if (returnOnFail) { return; }
this.toggleDrawDirection();
this.draw(e, block, true);
return;
}
this.ctx.lineTo(x, y);
this.ctx.stroke();
this.setCrosshair(x, y);
this.addPathToDrawSequence(x, y);
}
As you can see, I'm applying the '.grid' element over the canvas element. The grid element contains all the blocks that are displayed in flex mode. As you can see the grid container has a display:flex and flex-wrap: wrap properties. In this way, when the user clicks over a block, I can guess the x, y coordinates by getting its position, relative to the parent. Which has the same dimensions as the canvas. Once that I have the x,y coords, i can draw on the canvas.
Yes, creating a multiplicity of DOM elements and trying to dynamically position and size them with javascript will not be particularly performant. I don't think hr elements will solve this problem for you.
First, have you considered drawing your grid directly onto the canvas?
Another option is to have a background image with the grid on it layered behind the canvas. This will automatically resize just as performantly as any other aspect of your webpage.
Now for the 'snapping' part. It looks like you've already figured out how to draw what you need on the canvas once you get the grid information you're looking for. What you need is a method to get which grid a user clicked on. I'm guessing that is why you overlaid all those divs...
Instead, canvas natively tracks mouse clicks. Using some techniques laid out here should be able to get you the grid interaction information you're looking for.
Edit: A method to generate and find grids:
var height = 100;
var width = 200;
var horizontal_grids = 8;
var vertical_grids = 4;
function bounding_grid_1d(length, grids, x) {
var divisions = [];
var grid_width = length / grids;
for ( i = 0; i <= grids; i++ ) {
if (x || x == 0) {
if (i*grid_width > x) {
divisions.push((i-1)*grid_width);
divisions.push(i*grid_width);
break;
}
else if (i*grid_width == x) {
divisions.push(i*grid_width);
break;
}
}
else {
divisions.push(i*grid_width);
}
}
return divisions;
}
console.log("Get all the x and y grid line locations");
console.log(bounding_grid_1d(width, horizontal_grids));
console.log(bounding_grid_1d(height, vertical_grids));
console.log("Get the x and y grid line locations that surround the coordinates (60,30)");
console.log(bounding_grid_1d(width, horizontal_grids, 60));
console.log(bounding_grid_1d(height, vertical_grids, 30));
I want to create a media control bar for a video player with the controls blurring the background. Currently I can get the glassy look but I can't get blurring to work.
I tried following this guide: https://medium.com/#AmJustSam/how-to-do-css-only-frosted-glass-effect-e2666bafab91 but it seems like it only works on static images.
Here's an example of what I want:
I also saw the webkit backdrop-filter which looks perfect but it's only stable on safari so I can't use that. Any advice for frosted glass on dynamic videos?
I was able to accomplish this by copying the bottom part of the video to a canvas every frame and blurring the canvas via CSS. It seems to work well in Chrome, but flashes sometimes in Firefox. Doing the blur programmatically in the canvas with something like Superfast Blur might be more performant, but that's an experiment for another day. Tomorrow, probably.
function initControls (player, blurRadius, controlHeight, videoWidth, videoHeight) {
// crop player to video size
let video = player.querySelector('video');
videoWidth = videoWidth || video.clientWidth;
videoHeight = videoHeight || video.clientHeight;
player.style.width = videoWidth + 'px';
player.style.height = videoHeight + 'px';
// crop control bar to video size
let controlBar = player.querySelector('.control-bar');
controlBar.style.width = videoWidth + 'px';
controlBar.style.height = controlHeight + 'px';
// canvas needs to be slightly taller than what gets displayed
// to blur cleanly
let canvas = player.querySelector('canvas');
canvas.width = videoWidth;
canvas.height = 2 * blurRadius + controlHeight;
canvas.style.filter = `blur(${blurRadius}px)`;
canvas.style.top = -2 * blurRadius + 'px';
// copy video to canvas
let ctx = canvas.getContext('2d');
let videoCropY = videoHeight - canvas.height;
function updateCanvas () {
ctx.drawImage(
video,
0, videoCropY, canvas.width, canvas.height,
0, 0, canvas.width, canvas.height
);
}
// update the canvas only when necessary
let hovering = false;
function renderLoop () {
updateCanvas();
if (hovering && !video.paused) {
window.requestAnimationFrame(renderLoop);
}
}
// no point in rendering to a canvas you can't see
player.addEventListener('mouseenter', () => {
hovering = true;
renderLoop();
});
player.addEventListener('mouseleave', () => { hovering = false; });
video.addEventListener('play', renderLoop);
}
document.addEventListener('DOMContentLoaded', event => {
// do the magic
initControls(document.querySelector('.player'), 4, 50, 320, 240);
// basic play button functionality
document.querySelector('.play-button').addEventListener('click', event => {
let v = event.target.closest('.player').querySelector('video');
if (v.ended) v.currentTime = 0;
if (v.paused) {
v.play();
} else {
v.pause();
}
});
});
/* styling required for blurred control background */
.player {
position: relative;
}
.player > video {
margin: 0;
display: block;
}
.control-bar > canvas {
margin: 0;
display: block;
left: 0;
position: absolute;
}
.control-bar {
margin: 0;
overflow: hidden;
position: absolute;
bottom: 0;
/* height of control bar is specified in javascript, sorry about that */
}
/* simple control-hiding mechanism; other methods also work */
/* javascript relies on mouseover and mouseout to decide whether to update canvas */
.player > .control-bar {
display: none;
}
.player:hover > .control-bar {
display: block;
}
/* styling actual controls; adjust to taste */
.play-button {
height: 30px;
line-height: 30px;
width: 100px;
color: white;
border: 1px solid white;
position: absolute;
top: 10px;
left: 50%;
margin-left: -50px;
text-align: center;
}
<div class="player">
<video src="https://archive.org/download/MedicalA1950/MedicalA1950_512kb.mp4"></video>
<div class="control-bar">
<canvas></canvas>
<div class="play-button">PLAY/PAUSE</div>
</div>
</div>
I've been looking at using three.js for a fun experiment on a site. I would like to use a current experiment (for which I already have the code for) and use it as a background for my site.
Anybody know how to do this?
I saw it done here: http://janjorissen.be/
Three JS API: https://github.com/mrdoob/three.js/wiki/API-Reference
I'm going to add yet another answer. I'd use
canvas {
width: 100vw;
height: 100vh;
display: block;
position: fixed;
top: 0;
left: 0;
z-index: -9999;
}
Here's why:
Many people use canvas { width: 100%; height: 100% } but that arguably doesn't make a lot of sense. You don't want the canvas to be 100% of the body. You want it to 100% of the screen/window. That's what canvas { width: 100vw; height: 100vh; } does. It's 100% of the viewport width and viewport height.
This means you don't need to set the body to height: 100% which also would not make sense, especially if the page is taller than the window/screen
display: block; fixes some issues with scrollbars on certain browsers. Some pages use html, body { overflow: none; } but again that doesn't make sense if your page ends up needing to be taller than the screen/window.
position: fixed; makes the canvas position relative to the top of window so it won't scroll with the page. If you use position: absolute then the canvas will scroll off the top if the page is taller than the screen/window. For example this page.
top: 0; left 0; puts it at the top left. Without that it would default to it's default position which is inside the body's margins. Often this is solved by setting body { margin: 0; } but generally that means you end up needing some other container to add a margin back in otherwise your normal content gets positioned at the edge of the window.
z-index: -9999; is there to try to force it further back than anything else just in case the page itself is using some negative values for z-index
Here's an example as a snippet
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
var canvas = document.querySelector("canvas");
var renderer = new THREE.WebGLRenderer({canvas: canvas});
renderer.setClearColor(0xF0F0F0);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
wireframe: true,
});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 1;
function resize() {
var width = canvas.clientWidth;
var height = canvas.clientHeight;
if (width != canvas.width || height != canvas.height) {
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
}
function render(time) {
time *= 0.001;
resize();
cube.rotation.x = time;
cube.rotation.y = time * 0.31;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
canvas {
width: 100vw;
height: 100vh;
display: block;
position: fixed;
top: 0;
left: 0;
z-index: -9999;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r77/three.min.js"></script>
<canvas></canvas>
<div>
some content that is in front of the canvas
</div>
And here's an example outside SO so you can view it easier full size.
iframes work as well
Note that there's the issue that if your canvas animation is interactive the elements in front of the canvas will eat the mouse/touch events. There's no easy solution I know of for that. You can mark everything but that canvas/iframe as pointer-events: none and mark the canvas/iframe as pointer-events: auto but then you run into the issue that no text on your page can be selected and no links can be clicked. You could then say set <a> tags to have pointer-events: auto so links work but I'm sure there will be issues here and there depending on what info is on your page (trying to copy an email address, or a location address, etc...)
One note: most three.js examples are structured different (less flexible) by referencing window.innerWidth and window.innerHeight and putting the canvas inside a div with an id="canvas" for some reason.
Here's a snippet using that structure. There's several more lines of code, redundant calls to renderer.setSize and setting the camera aspect in 2 places (not very D.R.Y.) but as far as this Q&A is concerned the only difference is #canvas instead of canvas as the CSS to size the div instead of the canvas.
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer();
document.getElementById("canvas").appendChild(renderer.domElement);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xF0F0F0);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
wireframe: true,
});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 1;
function onResize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
function render(time) {
time *= 0.001;
cube.rotation.x = time;
cube.rotation.y = time * 0.31;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
#canvas {
width: 100vw;
height: 100vh;
display: block;
position: fixed;
top: 0;
left: 0;
z-index: -9999;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r77/three.min.js"></script>
<div id="canvas"></div>
<div>
some content that is in front of the canvas
</div>
usually i use iframe for that. Thus you dont have conflict with the base page.
<style>
iframe {
z-index : -9999;
position: absolute;
top : 0;
left : 0;
width : 100%;
height : 100%;
margin : 0;
padding : 0;
}
</style>
<iframe src="http://example.com/"></iframe>
an example of it
https://github.com/jeromeetienne/www.jetienne.com/blob/master/index-webgl.html#L128 for the source
http://jetienne.com/index-webgl.html for the living code
This is not an actual background, but a 100% width/height element that is displaying the animation, with the rest of the content "elevated" using z-index or similar above that fake background.
Following the very basic example on threejs.org (here), I only had to change the canvas style section to:
canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -9999;
}
That moved the canvas to the background.