HTML Canvas - Repeat a Tile

Started by Heretic86, November 22, 2020, 05:06:57 am

Previous topic - Next topic

Heretic86

November 22, 2020, 05:06:57 am Last Edit: November 22, 2020, 05:31:57 am by Heretic86
Color me stumped.

Im playing around with Canvas in HTML.  Basically a knockoff of RMXP Tile Editor.

https://www.webucate.me/canvas/editor.html

The trouble I am having is with the BOX Draw Mode.  When I draw the box, I cant quite finger out how to get the drawn map to repeat tiles correctly, although it DOES repeat tiles.

- Click teh "Toggle Tiles" button.  New window should appear.  Once the Tilemap loads, the "Loading" msg should go away.
- Select SEVERAL TILES to map as a Pattern in the "Tile Paints" window
- Click the "BOX" button.
- Click and DRAG the mouse on the map to draw.
- When drawing with either a -X or -Y vale from where you first clicked, the tiles do not repeat like they do in RMXP.

In my source code (just right click and view source) look at line 297 please.

I will only leave this alone for so long so dont be too surprised if this page gets changed soon.

Any help would be appreciated.

Yes, I am still around but only barely...

---

Edit: Nevermind.  I think I got it.

Next question, how to do a Circle / Oval?  Also a bit stumped there...  I had to put this ALL down for a couple years so I am supremely rusty...
Current Scripts:
Heretic's Moving Platforms

Current Demos:
Collection of Art and 100% Compatible Scripts

(Script Demos are all still available in the Collection link above.  I lost some individual demos due to a server crash.)

KK20

You can probably try to repurpose the algorithm shared here
https://stackoverflow.com/a/54488533

Blizzard has an editor for MV that I think has the ellipse draw tool implemented. I can ping him in the discord channel if you want (unless he see this himself).

Other Projects
RPG Maker XP Ace  Upgrade RMXP to RMVXA performance!
XPA Tilemap  Tilemap rewrite with many features, including custom resolution!

Nintendo Switch Friend Code: 8310-1917-5318
Discord: KK20 Tyler#8901

Join the CP Discord Server!

Heretic86

Yes, please ping him...

Here is my function so far...

function setTileDataEllipse(){
  angular.copy(mapTemp, map);

  let startX = mouseDownX;
  let startY = mouseDownY;
  let endX = mouseDownX;
  let endY = mouseDownY;
 
  if (mousePosX === null || mousePosY === null){
    startX = endX = mouseDownX;
    startY = endY = mouseDownY;
  }
  else {
    startX = Math.min(mouseDownX, mousePosX);
    startY = Math.min(mouseDownY, mousePosY);
    endX = Math.max(mouseDownX, mousePosX);
    endY = Math.max(mouseDownY, mousePosY);
  }
  let width = endX - startX;
  let height = endY - startY;
 
  //void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle [, anticlockwise]); 

  let offsetX = (mousePosX !== null && mousePosX <= mouseDownX) ? mousePosX - (mouseDownX % tile.width) : 0;
  let offsetY = (mousePosY !== null && mousePosY <= mouseDownY) ? mousePosY - (mouseDownY % tile.height) : 0;
  offsetX = (offsetX < 0) ? offsetX + tile.width : offsetX;
  offsetY = (offsetY < 0) ? offsetY + tile.height : offsetY;

  for (let i = 0; i < height + 1; i++){
    if (typeof(map.data[selectedLayer][startX + i]) == 'undefined' && startX + i <= map.width / 32){ map.data[selectedLayer][startX + i] = []; }
    for (let j = 0; j < width + 1; j++){
      if (startX + j >= map.width / 32 || i + startY >= map.height / 32){ continue; }
      // if (do thing here to see if tile is in an ellipse){
        map.data[selectedLayer][startX + j][startY + i] = tile.ids[(i + offsetY) % tile.height][(j + offsetX) % tile.width];
      //}
    }
  }
  drawLayersToCanvas();
}

Basically, I want to start from whatever corner a user starts drawing at and have the opposite corner be the users current mouse position.  If a tile is inside the ellipse, change the tile, if not, go to the next tile.  Basically just like the RMXP Editor with Ellipses.

Hopefully easy?  The math is over my head and havent had much time... Grandkid...  Im sure you understand the limited time, and my absence for the last few years...

Current Scripts:
Heretic's Moving Platforms

Current Demos:
Collection of Art and 100% Compatible Scripts

(Script Demos are all still available in the Collection link above.  I lost some individual demos due to a server crash.)

Blizzard

November 23, 2020, 04:33:36 am #3 Last Edit: November 23, 2020, 04:35:23 am by Blizzard
This is what G_G wrote for MOXIE (it's C#):

		private void UpdateEllipse()
{
if (MouseLeftButtonDown)
{
UndoTileChanges(MouseDownTileChanges);
MouseDownTileChanges.Clear();

MouseRect = Extensions.RectFromPoints(new Point(MouseStart.X * RPGData.TileSize, MouseStart.Y * RPGData.TileSize),
new Point(MouseTile.X * RPGData.TileSize, MouseTile.Y * RPGData.TileSize));
Vector2 pos = ScreenToMap(Vector2.Zero);
int startTileX = MouseStart.X;
int startTileY = MouseStart.Y;
int startX = Math.Min(MouseStart.X, MouseTile.X).Clamp(0, Map.Width - 1);
int startY = Math.Min(MouseStart.Y, MouseTile.Y).Clamp(0, Map.Height - 1);
int endX = Math.Max(MouseStart.X, MouseTile.X).Clamp(0, Map.Width - 1);
int endY = Math.Max(MouseStart.Y, MouseTile.Y).Clamp(0, Map.Height - 1);

if (EyedropperActive)
{
if (!eyedropperIsLeftCorner) startTileX -= eyedropperAreaWidth - 1;
if (!eyedropperIsTopCorner) startTileY -= eyedropperAreaHeight - 1;
}

float radX = (1 + endX - startX) / 2f;
float radY = (1 + endY - startY) / 2f;
float centerX = startX + radX;
float centerY = startY + radY;
for (int x = startX; x <= endX; x++)
{
float xrad = (0.5f + x - centerX) / radX;
for (int y = startY; y <= endY; y++)
{
float yrad = (0.5f + y - centerY) / radY;
if (xrad * xrad + yrad * yrad > 1)
continue;

if (EyedropperActive)
{
int dx = (x - startTileX) % eyedropperAreaWidth;
int dy = (y - startTileY) % eyedropperAreaHeight;

if (dx < 0) dx += eyedropperAreaWidth;
if (dy < 0) dy += eyedropperAreaHeight;

int index = dy * eyedropperAreaWidth + dx;
var newTile = eyedropperTiles[index];

var tileChange = Map.SetTile(SelectedLayer, x, y, newTile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}
else if (AutotileSelected)
{
var tileChange = Map.SetTile(SelectedLayer, x, y, AutotileSelector.Tile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}
else
{
int dx = (x - startTileX) % TilesetSelector.SelectionWidth;
int dy = (y - startTileY) % TilesetSelector.SelectionHeight;

if (dx < 0) dx += TilesetSelector.SelectionWidth;
if (dy < 0) dy += TilesetSelector.SelectionHeight;

int selectorIndex = dy * TilesetSelector.SelectionWidth + dx;
var newTile = TilesetSelector.Tiles[selectorIndex];

var tileChange = Map.SetTile(SelectedLayer, x, y, newTile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}
}
}

}

}

It's basically just some standard math. The code takes the area, calculates the center, then calculates an ellipse radius from that center and fills all the tiles that are within that radius.
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

Heretic86

Wow!  That was simpler than I expected!

Thanks as always Blizz!

Hope you all are doing well!
Current Scripts:
Heretic's Moving Platforms

Current Demos:
Collection of Art and 100% Compatible Scripts

(Script Demos are all still available in the Collection link above.  I lost some individual demos due to a server crash.)

Blizzard

Yeah, I'm good in these trying times. Working from home for the past few weeks and so on. You take care, too. :)
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

Heretic86

December 18, 2020, 12:16:19 am #6 Last Edit: December 18, 2020, 12:21:44 am by Heretic86
So next question, any suggestions on an efficient flood fill function?

NOTE: if you look at the original post link, it MAY or MAY NOT work as I am playing with it.

Edit.  Playing with it... that came out wrong...
Current Scripts:
Heretic's Moving Platforms

Current Demos:
Collection of Art and 100% Compatible Scripts

(Script Demos are all still available in the Collection link above.  I lost some individual demos due to a server crash.)

KK20

If your map sizes are relatively small and/or you don't care about performance, then recursion. It's really as simple as checking your cartesian coordinates and calling your recursive method on each space. A simple google search shows this as the most common implementation.

Otherwise, you should look into QuickFill
https://www.codeproject.com/Articles/6017/QuickFill-An-efficient-flood-fill-algorithm

Other Projects
RPG Maker XP Ace  Upgrade RMXP to RMVXA performance!
XPA Tilemap  Tilemap rewrite with many features, including custom resolution!

Nintendo Switch Friend Code: 8310-1917-5318
Discord: KK20 Tyler#8901

Join the CP Discord Server!

Blizzard

Here's the MOXIE one if you want. xD

		private void Fill(int x, int y)
{
if (!Map.ValidCoordinates(x, y)) return;
RPG.Tile oldTile = Map.GetTile(SelectedLayer, x, y);

if (AutotileSelected && oldTile == AutotileSelector.Tile) return;
if (!AutotileSelected && oldTile == TilesetSelector.Tiles[0]) return;

SortedSet<Point> tiles = new SortedSet<Point>(PointComparer.Singleton);
List<Point> newTiles = new List<Point>();
List<Point> newTilesCopy = new List<Point>();
Point start = new Point(x, y);
if (tiles.Add(start)) newTiles.Add(start);

int startTileX = x;
int startTileY = y;
if (EyedropperActive)
{
if (!eyedropperIsLeftCorner) startTileX -= eyedropperAreaWidth - 1;
if (!eyedropperIsTopCorner) startTileY -= eyedropperAreaHeight - 1;
}

while (newTiles.Count != 0)
{
newTilesCopy.Clear();
newTilesCopy.AddRange(newTiles);
newTiles.Clear();
foreach (var pos in newTilesCopy)
{
RPG.Tile tile = Map.GetTile(SelectedLayer, pos.X, pos.Y);
if (tile == oldTile)
{
if (EyedropperActive)
{
int dx = (pos.X - startTileX) % eyedropperAreaWidth;
int dy = (pos.Y - startTileY) % eyedropperAreaHeight;

if (dx < 0) dx += eyedropperAreaWidth;
if (dy < 0) dy += eyedropperAreaHeight;

int index = dy * eyedropperAreaWidth + dx;
var newTile = eyedropperTiles[index];

var tileChange = Map.SetTile(SelectedLayer, x, y, newTile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}
else if (AutotileSelected)
{
var tileChange = map.SetTile(SelectedLayer, pos.X, pos.Y, AutotileSelector.Tile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}
else
{
int dx = (pos.X - startTileX) % TilesetSelector.SelectionWidth;
int dy = (pos.Y - startTileY) % TilesetSelector.SelectionHeight;

if (dx < 0) dx += TilesetSelector.SelectionWidth;
if (dy < 0) dy += TilesetSelector.SelectionHeight;

int selectorIndex = dy * TilesetSelector.SelectionWidth + dx;
var newTile = TilesetSelector.Tiles[selectorIndex];
var tileChange = Map.SetTile(SelectedLayer, pos.X, pos.Y, newTile);
if (tileChange != null)
MouseDownTileChanges.Add(tileChange);
}

// add surrounding tiles to list
if (Map.ValidCoordinates(pos.X + 1, pos.Y))
{
var newPos = new Point(pos.X + 1, pos.Y);
if (tiles.Add(newPos)) newTiles.Add(newPos);
}
if (Map.ValidCoordinates(pos.X - 1, pos.Y))
{
var newPos = new Point(pos.X - 1, pos.Y);
if (tiles.Add(newPos)) newTiles.Add(newPos);
}
if (Map.ValidCoordinates(pos.X, pos.Y + 1))
{
var newPos = new Point(pos.X, pos.Y + 1);
if (tiles.Add(newPos)) newTiles.Add(newPos);
}
if (Map.ValidCoordinates(pos.X, pos.Y - 1))
{
var newPos = new Point(pos.X, pos.Y - 1);
if (tiles.Add(newPos)) newTiles.Add(newPos);
}

}
}
}

if (MouseDownTileChanges.Count != 0)
{
History.Add(new TileHistoryItem(Map, MouseDownTileChanges, MouseDownTileChangeDescription));
MoxieForm?.UpdateHistory();

MouseDownTileChanges.Clear();
}
}

IDK how it compares to QuickFill.
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

Heretic86

Ok, next question:

I have a javascript Class.  I would like the constructor to call a method to load graphics that finishes setting up once the graphics load.  I am trying to pass a callback function so I can resule the image loader.  I can not get the callback function to work at all, as a defined function, as an anonymous function, or with lambda.

Suggestions?

class Myclass {
  constructor(myCanvas){
    this.canvas = myCanvas;
    this.ctx = this.canvas.getContext('2d');
    this.myImg = this.loadGraphic('images/myimg.png' this.setupBars);
  }
  loadGraphic(filename, callback){
    let img = new Image();
    img.src = filename;
    img.onload = () => this.setupBars();                   // callback arg ignored, This works but can not define the callback
    //img.onload = () => { this.setupBars(); }             // Works same as above, dont need {} characters
    //img.onload = this.setupBars();                       // callback arg ignored, Argument 1 could not be converted to any of Graphics
    //img.onload = callback();                             // callback = setupBars, setupBars is not defined
    //img.onload = callback();                             // callback = this.setupBars, this is undefined
    //img.onload = this.callback();                        // callback = setupBars, setupBars is undefined
    //img.onload = this.callback();                        // callback = this.setupBars, this.callback is not a function
    //img.onload = () => callback();                       // callback = this.setupBars, 'this' is undefined
    //img.onload = () => callback();                       // callback = setupBars, setupBars is not defined
    //img.onload = () => this.callback();                  // callback = setupBars, setupBars is not defined
    //img.onload = (this) => callback();                   // callback = this.setupBars, missing formal parameter
    //img.onload = (this.callback) => callback();          // callback = this.setupBars, missing formal parameter
    //img.onload = (callback) => this.callback();          // callback = this.setupBars, this.callback is not a function
    //img.onload = (callback) => this.callback();          // callback = setupBars, setupBars is not defined
    //img.onload = (this) => callback();                   // callback = setupBars, missing formal parameter
    //img.onload = (callback) => callback();               // callback = setupBars, callback is not a function
    //img.onload = (callback) => callback();               // callback = this.setupBars, callback is not a function
    //img.onload = function(){ this.setupBars() };         // callback arg ignored, 'this.setupBars' is not a function
    //img.onload = function(this){ this.setupBars() };     // callback arg ignored, missing formal parameter
    //img.onload = function(){ callback() };               // 'this' is undefined
    //img.onload = function(callback){ callback() };       // callback = setupBars, setupBars is not defined
    //img.onload = function(callback){ callback() };       // callback = this.setupBars, callback is not a function
    //img.onload = function(this.callback){ callback() };  // callback = setupBars, Scrollbars is not defined
    //img.onload = function(callback){ this.callback() };  // callback = setupBars, setupBars is not defined
    //img.onload = function(callback){ callback() };       // callback = this.setupBars, callback is not a function
    return img;
  }
  setupBars(){
    // does drawing to canvas with loaded graphic
  }
Current Scripts:
Heretic's Moving Platforms

Current Demos:
Collection of Art and 100% Compatible Scripts

(Script Demos are all still available in the Collection link above.  I lost some individual demos due to a server crash.)

Blizzard

January 19, 2021, 06:07:10 pm #10 Last Edit: January 19, 2021, 06:08:43 pm by Blizzard
I think you need to use bind() if you want "this" to be available within the callback function call.

img.onload = this.setupBars.bind(this);

Or alternatively:

img.onload = (function() { this.setupBars(); }).bind(this);

But IDK if this second variant will work right.
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.