Explore all Canvas open source software, libraries, packages, source code, cloud functions and APIs.

Popular New Releases in Canvas

fabric.js

Version 5.2.1

node-canvas

v2.9.0

signature_pad

v4.0.4

F2

4.0.11

topology

Popular Libraries in Canvas

fabric.js

by fabricjs doticonjavascriptdoticon

star image 21561 doticonNOASSERTION

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser

node-canvas

by Automattic doticonc++doticon

star image 8313 doticon

Node canvas is a Cairo backed Canvas implementation for NodeJS.

signature_pad

by szimek doticontypescriptdoticon

star image 7897 doticonMIT

HTML5 canvas based smooth signature drawing

dom-to-image

by tsayen doticonjavascriptdoticon

star image 7728 doticonNOASSERTION

Generates an image from a DOM node using HTML5 canvas

F2

by antvis doticontypescriptdoticon

star image 7660 doticonMIT

📱📈An elegant, interactive and flexible charting library for mobile.

piskel

by piskelapp doticonjavascriptdoticon

star image 7530 doticonApache-2.0

A simple web-based tool for Spriting and Pixel art.

jsnes

by bfirsh doticonjavascriptdoticon

star image 5431 doticonApache-2.0

A JavaScript NES emulator.

canvas-lms

by instructure doticonrubydoticon

star image 4363 doticonNOASSERTION

The open LMS by Instructure, Inc.

sketch.js

by soulwire doticonjavascriptdoticon

star image 3890 doticonMIT

Cross-Platform JavaScript Creative Coding Framework

Trending New libraries in Canvas

perfect-freehand

by steveruizok doticontypescriptdoticon

star image 1735 doticonMIT

Draw perfect pressure-sensitive freehand strokes.

skia-canvas

by samizdatco doticonjavascriptdoticon

star image 956 doticonMIT

A canvas environment for Node.js

canvas

by Brooooooklyn doticonrustdoticon

star image 751 doticonMIT

High performance skia binding to Node.js. Zero system dependencies and pure npm packages without any postinstall scripts nor node-gyp.

generative-art-node

by HashLips doticonjavascriptdoticon

star image 640 doticonMIT

Create generative art by using the canvas api and node js

blobity

by gmrchk doticontypescriptdoticon

star image 590 doticonNOASSERTION

The cursor is the heart of any interaction with the web. Why not take it to the next level? 🚀

easy-canvas

by Gitjinfeiyang doticonjavascriptdoticon

star image 467 doticonMIT

使用render函数在canvas中创建文档流布局,小程序海报图、小程序朋友圈分享图。easy-canvas is a powerful tool helps us easy to layout with canvas.

generative-art-opensource

by HashLips doticonjavascriptdoticon

star image 442 doticonMIT

Create generative art by using the canvas api and node js, feel free to contribute to this repo with new ideas.

generativeart

by jdxyw doticongodoticon

star image 440 doticonMIT

Generative Art in Go

idraw

by idrawjs doticontypescriptdoticon

star image 432 doticonMIT

A simple JavaScript framework for Drawing on the web.(一个面向Web绘图的JavaScript框架)

Top Authors in Canvas

1

mattdesl

10 Libraries

star icon301

2

component

10 Libraries

star icon181

3

hughsk

8 Libraries

star icon167

4

Jam3

7 Libraries

star icon60

5

spritejs

7 Libraries

star icon277

6

codepo8

6 Libraries

star icon204

7

gre

6 Libraries

star icon148

8

rolyatmax

6 Libraries

star icon109

9

instructure

6 Libraries

star icon4922

10

clindsey

5 Libraries

star icon18

1

10 Libraries

star icon301

2

10 Libraries

star icon181

3

8 Libraries

star icon167

4

7 Libraries

star icon60

5

7 Libraries

star icon277

6

6 Libraries

star icon204

7

6 Libraries

star icon148

8

6 Libraries

star icon109

9

6 Libraries

star icon4922

10

5 Libraries

star icon18

Trending Kits in Canvas

React canvas libraries are plugins or frameworks. These enable developers to work with canvas graphics within React applications. They integrate the flexibility of Canvas API into the declarative and reactive React. These libraries are used for complex canvas graphics, game development, creative coding, etc.    

    

Different react canvas libraries are available, ranging from simple plugins to comprehensive frameworks. Some popular ones include React Konva, React Art, and react-canvas. These libraries offer different approaches and feature sets. Thus, developers can choose the one that best fits their project requirements.    

    

React canvas libraries come with a variety of features to facilitate canvas graphics. They provide components and APIs for drawing shapes, handling mouse events, and more. They often offer declarative and reactive bindings. These resemble React component model, making it easier to work with canvas graphics. Their standard features include comprehensive documentation, detailed examples, and active community support.    

    

Consider a few factors when choosing a react canvas library for your project. These include the complexity of graphics, performance considerations, and the availability of components. Test the library's documentation, community support, and compatibility with other technologies.    

    

React canvas libraries can be used in various ways depending on the project scope. They are suitable for developing applications with canvas graphics, like interactive image galleries. They can also be utilized for building more complex web applications. This can include game platforms, creative coding projects, and desktop or mobile applications.    

To use a react canvas library:   

  1. Start by installing the necessary dependencies through package managers like npm.    
  2. Follow the library's documentation to set up the components and understand the API.   
  3. Use the library's features to create canvas shapes, handle events, apply animations, etc.   

Real-world applications demonstrate the versatility of React canvas libraries. They are used to create:    

  • weather apps that display dynamic graphics based on weather data,    
  • gaming platforms that offer immersive gaming experiences, and    
  • interactive creative coding projects that push the boundaries of visual expression.    

    

Thus, react canvas libraries allow us to work with canvas graphics in React applications. They offer a range of features, from drawing shapes to handling events. They come in various forms, from simple plugins to comprehensive frameworks. Using React and the Canvas API, developers can create appealing and interactive applications.    

react-three-fiber:    

  • 3D visualizations, virtual reality (VR), and augmented reality (AR).    
  • Enables rendering of 3D scenes using React components.    
  • Supports integration with WebGL and Three.js library.    
  • Provides advanced features like lighting, shadows, and camera controls.    

react-canvas:    

  • High-performance graphics rendering, animations, and visual effects.    
  • Utilizes the HTML5 canvas element for rendering.    
  • Supports hardware acceleration for smooth animations.    
  • Provides a simple API for drawing shapes, text, and images.    

react-konva:     

  • Interactive games, data visualization, and image manipulation.     
  • Helps in creating and manipulating complex canvas-based graphics.    
  • Supports event handling for user interactions.    
  • Provides a declarative API for easier development.    

react-game-kit:    

  • Game development, physics simulations.    
  • Offers a set of components and utilities for building games.    
  • Provides a physics engine for realistic interactions.    
  • Supports sprite animations, input handling, and game state management.    

react-art:    

  • Vector graphics, charting, custom illustrations.    
  • Enables rendering of vector graphics using React components.    
  • Supports drawing paths, shapes, and text with SVG-like syntax.    
  • Integrates well with existing React projects.    

react-pixi-fiber:    

  • High-performance 2D graphics and game development.    
  • Integrates React with Pixi.js, a popular 2D rendering engine.    
  • Enables efficient rendering of large numbers of sprites.    
  • Supports interactivity, animations, and special effects.   

react-canvas-draw:    

  • Drawing and sketching applications.    
  • Offers a canvas-based drawing component for React.    
  • Supports freehand drawing, erasing, and color selection.    
  • Provides event hooks for capturing user input.    

react-sketch:    

  • Prototyping, wireframing, and design tools.    
  • Provides a canvas-based sketching component for React.    
  • Supports drawing shapes, lines, and text.    
  • Offers customization options for colors, sizes, and backgrounds.    

FAQ 

1. What is the canvas graphics library used for, and why is it so popular?    

A canvas graphics library works with graphics and animations within web applications. It helps with complex graphics using the HTML5 canvas element and Canvas API. They are popular for their flexibility in creating appealing and interactive web experiences.   

    

2. Is there a different React plugin to create canvas graphics instead of React Konva?    

React Konva is a popular canvas graphics library for React applications. While it is widely used and well-documented, other react plugins are available too. One such alternative is react-canvas. This provides a different approach and features to work with canvas graphics. Developers can explore different plugins and choose the best-suited one.   

    

3. How does a canvas library help React developers build complex web apps?    

A canvas library aids developers in creating complex web applications with React components. It provides tools and APIs designed for working with canvas graphics. These libraries offer components for drawing shapes, handling events, and managing animations. Developers can leverage React's declarative and reactive nature to create and manage graphics. Do this by integrating canvas libraries into React components.    

    

4. What is the current state of the react hype, and how will it grow?    

React is a popular and widely adopted technology in the web development community. The React hype refers to the enthusiasm around React usage for building applications. React thrives with a vibrant community, extensive docs, and support from Facebook & open-source. Its popularity will grow as more developers recognize its benefits and React evolves.    

    

5. How do browser events interact with React Konva to generate DOM-like objects?    

Browser events, like mouse events, interact with the React Konva library. This is done by being captured and handled by the library's event listeners. React Konva provides a DOM-like object model. Here the canvas shapes and elements are part of the browser's DOM. React Konva uses event listeners to address browser events on canvas shapes. Developers can update canvas graphics based on user interactions like clicks or drags. 

The Canvas widget in Python Tkinter is incredibly powerful and flexible. It allows you to create custom graphics, sketching tools, and complex widgets.


It provides a coordinate system for positioning canvas items within the canvas widget. The canvas coordinate system defines item positions using coordinates. It includes methods for managing display and scrolling like view, window, and screenx. It also supports line width, length, outline style, and fill color options. This can customize the appearance of items.  


The canvas widget implements freehand sketching tools for Sketchpad programs or drawing tools. It can also implement CAD-like functionalities. You can use different item types to create shapes like rectangles, polygons, and arcs. Additionally, you can work with bounding boxes to define canvas item areas precisely.  


You can use the canvas widget to control the mouse keyboard and make it move. This enables interactivity with canvas items. For instance, you can select and manipulate items based on mouse clicks and positions.  


To scroll the canvas, use the scrollbars and choose the scroll region option. This specifies the scrollable area. Furthermore, the canvas supports stipple patterns. It also supports transparency effects and various drawing tools like arrows and lines.  


To create a canvas widget in Python using the Tkinter library, import tkinter with   

from tkinter import *   

    (Or)  

import tkinter.  


You can create the main Tkinter window using the `tk.Tk()` constructor. After that, create the canvas widget using the `tk.Canvas()` constructor. Also, specify its width and height in pixels.   


For instance, you can create a canvas with a width of 400 pixels and a height of 300 pixels. Canvas = tk.Canvas(root, width=400, height=300)  

To display the canvas, you can add it to the main window using one of the geometry managers, such as `pack()`, `grid()`, or `place()`. For example, using the' pack' geometry manager, you can use `canvas.pack()` to add the canvas to the main window.  


Run the Tkinter main loop to make the Tkinter application visible and responsive. Call the `root.mainloop()`. This loop continuously listens for events and updates the GUI accordingly. It allows the canvas and other widgets to respond to user interactions.  


You can utilize event binding to add events to a canvas widget in Tkinter. It is a powerful mechanism that associates specific functions with events. For instance, use the' bind' method to handle mouse clicks on the canvas. This links it to a custom function that executes when a left mouse click occurs. The function can respond by printing the click coordinates.   


For shape movements, you would first need to implement the logic for moving the shape. Then, bind the necessary events to the canvas. For example, you can create a rectangle on the canvas. Then, use event binding to respond to mouse clicks on the shape and subsequent dragging of the shape. The program saves the click's coordinates when the user clicks on the rectangle. When you drag the mouse, update the shape's position based on the movement distance.  


By event binding, your canvas application becomes interactive and responsive to user actions. This includes button clicks or dynamic shape movements. This capability improves user experience and enables engaging graphical applications.  


Preview of the output that you will get on running this code from your IDE

Code

This code uses the `tkinter` library to create a 200x200 pixel white canvas. It draws a line from coordinates (0,0) to (100,100) on the canvas, forming a diagonal line. The `mainloop()` function keeps the GUI running until it's closed by the user.

Follow the steps carefully to get the output easily.

  • Download and install VS Code on your desktop.
  • Open VS Code and create a new file in the editor.
  • Copy the code snippet that you want to run, using the "Copy" button or by selecting the text and using the copy command (Ctrl+C on Windows/Linux or Cmd+C on Mac).,
  • Paste the code into your file in VS Code, and save the file with a meaningful name and the appropriate file extension for Python use (.py).file extension.
  • To run the code, open the file in VS Code and click the "Run" button in the top menu, or use the keyboard shortcut Ctrl+Alt+N (on Windows and Linux) or Cmd+Alt+N (on Mac). The output of your code will appear in the VS Code output console.


I hope you found this useful. Remember that Tkinter is an built-in library that comes along with python and doesn't require any installation.


I found this code snippet by searching for "canvas tkinter" in kandi. You can try any such use case!

Environment tested

I tested this solution in the following versions. Be mindful of changes when working with other versions.


  1. The solution is created and tested using Vscode 1.77.2 version
  2. This code was tested using Python version 3.8.0


By using this technique, you can make attractive startup screens for GUI apps in Python's Tkinter. This process also facilitates an easy-to-use, hassle-free method to create a hands-on working version of code. This helps to create canvas widgets using Tkinter in Python

FAQ

1. What can you do with a canvas widget in Python Tkinter Canvas?   

With Python Tkinter canvas, you can make and change graphics such as lines, shapes, text, and images. You can use it as a surface to draw and create interactive graphics and games.   


2. How does the canvas coordinate system work?   

The canvas coordinate system in Tkinter works with (0,0) as the top-left corner of the canvas. The x and y coordinates increase as you move right and down. We use negative coordinates to represent positions outside the visible canvas area.   


3. Could you give an example of a simple sketchpad in Python with Tkinter Canvas?   

import tkinter as tk;   

canvas = tk.Canvas(width=400, height=300);   

canvas.pack()   

This code imports Tkinter as `tk` and creates a canvas widget with a width of 400 pixels and a height of 300 pixels. It then packs it into the main window for display.  


4. Are there any limits when using rectangular items on a Python tkinter canvas?   

There are limitations when working with rectangle items on a Python Tkinter canvas. It includes limited support for complex transformations. It also includes limited event handling options compared to other shapes.   


5. How can one create polygon shapes on Python tkinter canvases?   

To create polygon shapes on a Python Tkinter canvas, use the `create_polygon()` method. Also, provide the coordinates of the vertices as arguments in the form of a list. For example: `canvas.create_polygon(x1, y1, x2, y2, x3, y3, ..., xn, yn)`  

Support

  1. For any support on kandi solution kits, please use the chat
  2. For further learning resources, visit the Open Weaver Community learning page.

<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <style>

    body {

      text-align: center;

      font-family: 'Arial', sans-serif;

    }


    #character {

      width: 200px;

      height: 300px;

      border: 2px solid #000;

      margin: 20px;

    }

  </style>

  <title>Anime Character Generator</title>

</head>

<body>

  <h1>Anime Character Generator</h1>

  <div id="character"></div>

  <button onclick="generateCharacter()">Generate Character</button>


  <script>

    const hairstyles = ["hairstyle1", "hairstyle2", "hairstyle3"];

    const eyecolors = ["blue", "green", "brown"];

    const outfits = ["outfit1", "outfit2", "outfit3"];


    function getRandomElement(array) {

      const randomIndex = Math.floor(Math.random() * array.length);

      return array[randomIndex];

    }


    function generateCharacter() {

      const hairstyle = getRandomElement(hairstyles);

      const eyeColor = getRandomElement(eyeColors);

      const outfit = getRandomElement(outfits);


      const characterDiv = document.getElementById("character");

      characterDiv.innerHTML = `

        <div style="background-color: ${eyeColor}; height: 20px;"></div>

        <div style="background-image: url('${hairstyle}.png'); height: 100px;"></div>

        <div style="background-image: url('${outfit}.png'); height: 180px;"></div>

      `;

    }

  </script>

</body>

</html>



Generating anime characters using JavaScript involves creating a program that can randomly generate various attributes such as hairstyles, eye colors, clothing, and other features typical of anime characters. You can achieve this by creating a web page with HTML for the structure and CSS for styling, and then using JavaScript to handle the logic of character generation.


In this example, replace "hairstyle1.png", "hairstyle2.png", etc., and "outfit1.png", "outfit2.png", etc., with the actual file paths of your hairstyle and outfit images. You'll need to have image files for different hairstyles, eye colors, and outfits.

This is a basic example, and you can expand on it by adding more features, such as different facial expressions, accessories, and so on. You can also consider using a backend server to store a larger set of assets and dynamically load them into your character generator.

Trending Discussions on Canvas

Animate needle transition

How To Scale The Contents Of A UIView To Fit A Destination Rectangle Whilst Maintaining The Aspect Ratio?

Prevent y-axis labels from being cut off

Javascript: frame precise video stop

Opening PDFs in WebView2 based on selection in CheckBoxColumn

Is it possible to manually update the value of a Behaviour? (Functional Reactive Programming, Threepenny)

Problem resizing plot on tkinter figure canvas

Efficient code for custom color formatting in tkinter python

Android: Iterative queue-based flood fill algorithm 'expandToNeighborsWithMap()' function is unusually slow

create a circle object and push them in array

QUESTION

Animate needle transition

Asked 2022-Mar-21 at 22:09

When I read data from GPS sensor, it comes with a slight delay. You are not getting values like 0,1 0,2 0,3 0,4 0,5 etc, but they are coming like 1 then suddenly 5 or 9 or 12. In this case needle is jumping back and forth. Anybody have an idea how to make needle moving smoothly? I guess some kind of delay is needed?

Something like, taken from another control:

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12

However I am a bit confused how to implement that.

Here is code for drawing a needle on canvas:

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43

EDIT:

Still having hard times to understand the process.

Let's say I don't want to apply this filter for control but have it in ViewModel to filter value. I have a Class from where I am getting data, for example GPSTracker. GPSTracker provides speed value, then I am subscribing to EventListener in my HomeViewModel and want to filter incoming value.

Based on Adams answer:

ANSWER

Answered 2022-Mar-21 at 22:09

Coming from a controls background, to mimic behavior of an analog device, you could use an exponential (aka low-pass) filter.

There are two types of low-pass filters you can use, depending on what type of behavior you want to see: a first-order or second-order filter. To put it in a nutshell, if your reading was steady at 0 then suddenly changed to 10 and held steady at 10 (a step change), the first order would slowly go to 10, never passing it, then remain at 10 whereas the second order would speed up its progress towards 10, pass it, then oscillate in towards 10.

The function for an exponential filter is simple:

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
44{
45    if (time_passed &gt; 0.0)
46    {
47        if (time_constant &gt; 0.0)
48        {
49            source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
50        }
51        filtered_value = source_value;
52    }
53}
54

filtered_value is the filtered version of the source source_value, time_passed is how much time passed from the last time this function was called to filter filtered_value, and time_constant is the time constant of the filter (FYI, reacting to a step change, filtered_value will get 63% of the way towards source_value after time_constant time has passed and 99% when 5x have passed). The units of filtered_value will be the same as source_value. The units of time_passed and time_constant need to be the same, whether this be seconds, microseconds, or jiffy. Additionally, time_passed should be significantly smaller than time_constant at all times, otherwise the filter behavior will become non-deterministic. There are multiple ways to get the time_passed, such as Stopwatch, see How can I calculate how much time have been passed?

Before using the filter function, you would need to initialize the filtered_value and whatever you use to get time_passed. For this example, I will use stopwatch.

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
44{
45    if (time_passed &gt; 0.0)
46    {
47        if (time_constant &gt; 0.0)
48        {
49            source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
50        }
51        filtered_value = source_value;
52    }
53}
54var stopwatch = new System.Diagnostics.Stopwatch();
55double filtered_value, filtered_dot_value;
56...
57filtered_value = source_value;
58filtered_dot_value = 0.0;
59stopwatch.Start();
60

To use this function for a first-order filter, you would loop the following using a timer or something similar

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
44{
45    if (time_passed &gt; 0.0)
46    {
47        if (time_constant &gt; 0.0)
48        {
49            source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
50        }
51        filtered_value = source_value;
52    }
53}
54var stopwatch = new System.Diagnostics.Stopwatch();
55double filtered_value, filtered_dot_value;
56...
57filtered_value = source_value;
58filtered_dot_value = 0.0;
59stopwatch.Start();
60double time_passed = stopwatch.ElapsedMilliseconds;
61stopwatch.Restart();
62Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
63

To use this function for a second-order filter, you would loop the following using a timer or something similar

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
44{
45    if (time_passed &gt; 0.0)
46    {
47        if (time_constant &gt; 0.0)
48        {
49            source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
50        }
51        filtered_value = source_value;
52    }
53}
54var stopwatch = new System.Diagnostics.Stopwatch();
55double filtered_value, filtered_dot_value;
56...
57filtered_value = source_value;
58filtered_dot_value = 0.0;
59stopwatch.Start();
60double time_passed = stopwatch.ElapsedMilliseconds;
61stopwatch.Restart();
62Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
63double time_passed = stopwatch.ElapsedMilliseconds;
64stopwatch.Restart();
65if (time_passed &gt; 0.0)
66{
67    double last_value = filtered_value;
68    filtered_value += filtered_dot_value * time_passed;
69    Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
70    Exp_Filt(ref filtered_dot_value, (filtered_value - last_value) / time_passed, time_passed, dot_time_constant);
71}
72

The second-order filter works by taking the first derivative of the first-order filtered value into account. Also, I would recommend making time_constant < dot_time_constant - to start, I would set dot_time_constant = 2 * time_constant

Personally, I would call this filter in a background thread controlled by a threading timer and have time_passed a constant equal to the timer's period, but I will leave the implementation specifics up to you.

EDIT:

Below is example class to create first and second order filters. To operate the filter, I use a threading timer set to process every 100 milliseconds. Being that this timer is rather consistent, making time_passed constant, I optimized the filter equation by pre-calculating Math.Exp(-time_passed / time_constant) and not dividing/multiplying 'dot' term by time_passed.

For first-order filter, use var filter = new ExpFilter(initial_value, time_constant). For second-order filter, use var filter = new ExpFilter(initial_value, time_constant, dot_time_constant). Then, to read latest filtered value, call double value = filter.Value. To set value to filter towards, call filter.Value = value.

1    async void animateProgress(int progress)
2    {
3        sweepAngle = 1;
4
5        // Looping at data interval of 5
6        for (int i = 0; i &lt; progress; i=i+5)
7        {
8            sweepAngle = i;
9            await Task.Delay(3);
10        }
11    }
12    private void OnDrawNeedle()
13    {
14        using (var needlePath = new SKPath())
15        {
16            //first set up needle pointing towards 0 degrees (or 6 o'clock)
17            var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
18            var needleOffset = ScaleToSize(NeedleOffset);
19            var needleStart = _center.Y - needleOffset;
20            var needleLength = ScaleToSize(NeedleLength);
21
22            needlePath.MoveTo(_center.X - widthOffset, needleStart);
23            needlePath.LineTo(_center.X + widthOffset, needleStart);
24            needlePath.LineTo(_center.X, needleStart + needleLength);
25            needlePath.LineTo(_center.X - widthOffset, needleStart);
26            needlePath.Close();
27
28            //then calculate needle position in degrees
29            var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
30
31            //finally rotate needle to actual value
32            needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
33
34            using (var needlePaint = new SKPaint())
35            {
36                needlePaint.IsAntialias = true;
37                needlePaint.Color = NeedleColor.ToSKColor();
38                needlePaint.Style = SKPaintStyle.Fill;
39                _canvas.DrawPath(needlePath, needlePaint);
40            }
41        }
42    }
43public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
44{
45    if (time_passed &gt; 0.0)
46    {
47        if (time_constant &gt; 0.0)
48        {
49            source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
50        }
51        filtered_value = source_value;
52    }
53}
54var stopwatch = new System.Diagnostics.Stopwatch();
55double filtered_value, filtered_dot_value;
56...
57filtered_value = source_value;
58filtered_dot_value = 0.0;
59stopwatch.Start();
60double time_passed = stopwatch.ElapsedMilliseconds;
61stopwatch.Restart();
62Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
63double time_passed = stopwatch.ElapsedMilliseconds;
64stopwatch.Restart();
65if (time_passed &gt; 0.0)
66{
67    double last_value = filtered_value;
68    filtered_value += filtered_dot_value * time_passed;
69    Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
70    Exp_Filt(ref filtered_dot_value, (filtered_value - last_value) / time_passed, time_passed, dot_time_constant);
71}
72    public class ExpFilter : IDisposable
73    {
74        private double _input, _output, _dot;
75        private readonly double _tc, _tc_dot;
76        private System.Threading.Timer _timer;
77
78        /// &lt;summary&gt;
79        /// Initializes first-order filter
80        /// &lt;/summary&gt;
81        /// &lt;param name=&quot;value&quot;&gt;initial value of filter&lt;/param&gt;
82        /// &lt;param name=&quot;time_constant&quot;&gt;time constant of filter, in seconds&lt;/param&gt;
83        /// &lt;exception cref=&quot;ArgumentOutOfRangeException&quot;&gt;&lt;paramref name=&quot;time_constant&quot;/&gt; must be positive&lt;/exception&gt;
84        public ExpFilter(double value, double time_constant)
85        {
86            // time constant must be positive
87            if (time_constant &lt;= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
88
89            // initialize filter
90            _output = _input = value;
91            _dot = 0.0;
92
93            // calculate gain from time constant
94            _tc = CalcTC(time_constant);
95
96            // disable second-order
97            _tc_dot = -1.0;
98
99            // start filter timer
100            StartTimer();
101        }
102
103        /// &lt;summary&gt;
104        /// Initializes second-order filter
105        /// &lt;/summary&gt;
106        /// &lt;param name=&quot;value&quot;&gt;initial value of filter&lt;/param&gt;
107        /// &lt;param name=&quot;time_constant&quot;&gt;time constant of primary filter, in seconds&lt;/param&gt;
108        /// &lt;param name=&quot;dot_time_constant&quot;&gt;time constant of secondary filter, in seconds&lt;/param&gt;
109        /// &lt;exception cref=&quot;ArgumentOutOfRangeException&quot;&gt;&lt;paramref name=&quot;time_constant&quot;/&gt; and &lt;paramref name=&quot;dot_time_constant&quot;/&gt; must be positive&lt;/exception&gt;
110        public ExpFilter(double value, double time_constant, double dot_time_constant)
111        {
112            // time constant must be positive
113            if (time_constant &lt;= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
114            if (dot_time_constant &lt;= 0.0) throw new ArgumentOutOfRangeException(nameof(dot_time_constant));
115
116            // initialize filter
117            _output = _input = value;
118            _dot = 0.0;
119
120            // calculate gains from time constants
121            _tc = CalcTC(time_constant);
122            _tc_dot = CalcTC(dot_time_constant);
123
124            // start filter timer
125            StartTimer();
126        }
127
128        // the following two functions must share the same time period
129        private double CalcTC(double time_constant)
130        {
131            // time period = 0.1 s (100 ms)
132            return Math.Exp(-0.1 / time_constant);
133        }
134        private void StartTimer()
135        {
136            // time period = 100 ms
137            _timer = new System.Threading.Timer(Filter_Timer, this, 100, 100);
138        }
139
140        ~ExpFilter()
141        {
142            Dispose(false);
143        }
144        public void Dispose()
145        {
146            Dispose(true);
147            GC.SuppressFinalize(this);
148        }
149        protected virtual void Dispose(bool disposing)
150        {
151            if (disposing)
152            {
153                _timer.Dispose();
154            }
155        }
156
157        /// &lt;summary&gt;
158        /// Get/Set filter value
159        /// &lt;/summary&gt;
160        public double Value
161        {
162            get =&gt; _output;
163            set =&gt; _input = value;
164        }
165
166        private static void Filter_Timer(object stateInfo)
167        {
168            var _filter = (ExpFilter)stateInfo;
169
170            // get values
171            double _input = _filter._input;
172            double _output = _filter._output;
173            double _dot = _filter._dot;
174
175            // if second-order, adjust _output (no change if first-order as _dot = 0)
176            // then use filter function to calculate new filter value
177            _input += (_output + _dot - _input) * _filter._tc;
178            _filter._output = _input;
179
180            if (_filter._tc_dot &gt;= 0.0)
181            {
182                // calculate second-order portion of filter
183                _output = _input - _output;
184                _output += (_dot - _output) * _filter._tc_dot;
185                _filter._dot = _output;
186            }
187        }
188    }
189

Source https://stackoverflow.com/questions/71493963

QUESTION

How To Scale The Contents Of A UIView To Fit A Destination Rectangle Whilst Maintaining The Aspect Ratio?

Asked 2022-Feb-16 at 15:42

I am trying to solve a problem without success and am hoping someone could help.

I have looked for similar posts but haven't been able to find anything which solves my problem.

My Scenario is as follows: I have a UIView on which a number of other UIViews can be placed. These can be moved, scaled and rotated using gesture recognisers (There is no issue here). The User is able to change the Aspect Ratio of the Main View (the Canvas) and my problem is trying to scale the content of the Canvas to fit into the new destination size.

There are a number of posts with a similar theme e.g:

calculate new size and location on a CGRect

How to create an image of specific size from UIView

But these don't address the changing of ratios multiple times.

My Approach:

When I change the aspect ratio of the canvas, I make use of AVFoundation to calculate an aspect fitted rectangle which the subviews of the canvas should fit:

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10

Test Cases The Red frame is simply to visualise the Aspect Fitted Rectangle. As you can see whilst the aspect fitted rectangle is correct, the scaling of objects isn't working. This is especially true when I apply scale and rotation to the subviews (CanvasElement).

The logic where I am scaling the objects is clearly wrong:

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38

I am enclosing the CanvasElement Class as well if this helps:

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38final class CanvasElement: UIView {
39  
40  var rotation: CGFloat = 0
41  var baseFrame: CGRect = .zero
42
43  var id: String = UUID().uuidString
44  
45  // MARK: - Initialization
46  
47  override init(frame: CGRect) {
48    super.init(frame: frame)
49    storeState()
50    setupGesture()
51  }
52  
53  required init?(coder aDecoder: NSCoder) {
54    super.init(coder: aDecoder)
55  }
56  
57  // MARK: - Gesture Setup
58  
59  private func setupGesture() {
60    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
61    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
62    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
63    addGestureRecognizer(panGestureRecognizer)
64    addGestureRecognizer(pinchGestureRecognizer)
65    addGestureRecognizer(rotateGestureRecognizer)
66  }
67  
68  // MARK: - Touches
69  
70  override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
71    super.touchesBegan(touches, with: event)
72    moveToFront()
73  }
74  
75  //MARK: - Gestures
76  
77  @objc
78  private func panGesture(_ sender: UIPanGestureRecognizer) {
79    let move = sender.translation(in: self)
80    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
81    sender.setTranslation(CGPoint.zero, in: self)
82    storeState()
83  }
84  
85  @objc
86  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
87    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
88    sender.scale = 1
89    storeState()
90  }
91  
92  @objc
93  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
94    rotation += sender.rotation
95    transform = transform.rotated(by: sender.rotation)
96    sender.rotation = 0
97    storeState()
98  }
99  
100  // MARK: - Miscelaneous
101  
102  func moveToFront() {
103    superview?.bringSubviewToFront(self)
104  }
105  
106  public func rotated(by degrees: CGFloat) {
107    transform = transform.rotated(by: degrees)
108    rotation += degrees
109  }
110  
111  func storeState() {
112    print(&quot;&quot;&quot;
113    Element Frame = \(frame)
114    Element Bounds = \(bounds)
115    Element Center = \(center)
116    &quot;&quot;&quot;)
117    baseFrame = frame
118  }
119}
120

Any help or advise, approaches, with some actual examples would be great. Im not expecting anyone to provide full source code, but something which I could use as a basis.

Thank you for taking the time to read my question.

ANSWER

Answered 2022-Feb-06 at 10:03

Here are a few thoughts and findings while playing around with this

1. Is the right scale factor being used?

The scaling you use is a bit custom and cannot be compared directly to the examples which has just 1 scale factor like 2 or 3. However, your scale factor has 2 dimensions but I see you compensate for this to get the minimum of the width and height scaling:

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38final class CanvasElement: UIView {
39  
40  var rotation: CGFloat = 0
41  var baseFrame: CGRect = .zero
42
43  var id: String = UUID().uuidString
44  
45  // MARK: - Initialization
46  
47  override init(frame: CGRect) {
48    super.init(frame: frame)
49    storeState()
50    setupGesture()
51  }
52  
53  required init?(coder aDecoder: NSCoder) {
54    super.init(coder: aDecoder)
55  }
56  
57  // MARK: - Gesture Setup
58  
59  private func setupGesture() {
60    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
61    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
62    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
63    addGestureRecognizer(panGestureRecognizer)
64    addGestureRecognizer(pinchGestureRecognizer)
65    addGestureRecognizer(rotateGestureRecognizer)
66  }
67  
68  // MARK: - Touches
69  
70  override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
71    super.touchesBegan(touches, with: event)
72    moveToFront()
73  }
74  
75  //MARK: - Gestures
76  
77  @objc
78  private func panGesture(_ sender: UIPanGestureRecognizer) {
79    let move = sender.translation(in: self)
80    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
81    sender.setTranslation(CGPoint.zero, in: self)
82    storeState()
83  }
84  
85  @objc
86  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
87    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
88    sender.scale = 1
89    storeState()
90  }
91  
92  @objc
93  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
94    rotation += sender.rotation
95    transform = transform.rotated(by: sender.rotation)
96    sender.rotation = 0
97    storeState()
98  }
99  
100  // MARK: - Miscelaneous
101  
102  func moveToFront() {
103    superview?.bringSubviewToFront(self)
104  }
105  
106  public func rotated(by degrees: CGFloat) {
107    transform = transform.rotated(by: degrees)
108    rotation += degrees
109  }
110  
111  func storeState() {
112    print(&quot;&quot;&quot;
113    Element Frame = \(frame)
114    Element Bounds = \(bounds)
115    Element Center = \(center)
116    &quot;&quot;&quot;)
117    baseFrame = frame
118  }
119}
120let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
121                aspectFittedFrame.size.height / canvas.frame.height)
122

In my opinion, I don't think this is the right scale factor. To me this compares new aspectFittedFrame with the new canvas frame

Aspect Ratio UIView translation scaling rotation Swift ios

when actually I believe the right scaling factor is to compare the new aspectFittedFrame with the previous canvas frame

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38final class CanvasElement: UIView {
39  
40  var rotation: CGFloat = 0
41  var baseFrame: CGRect = .zero
42
43  var id: String = UUID().uuidString
44  
45  // MARK: - Initialization
46  
47  override init(frame: CGRect) {
48    super.init(frame: frame)
49    storeState()
50    setupGesture()
51  }
52  
53  required init?(coder aDecoder: NSCoder) {
54    super.init(coder: aDecoder)
55  }
56  
57  // MARK: - Gesture Setup
58  
59  private func setupGesture() {
60    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
61    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
62    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
63    addGestureRecognizer(panGestureRecognizer)
64    addGestureRecognizer(pinchGestureRecognizer)
65    addGestureRecognizer(rotateGestureRecognizer)
66  }
67  
68  // MARK: - Touches
69  
70  override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
71    super.touchesBegan(touches, with: event)
72    moveToFront()
73  }
74  
75  //MARK: - Gestures
76  
77  @objc
78  private func panGesture(_ sender: UIPanGestureRecognizer) {
79    let move = sender.translation(in: self)
80    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
81    sender.setTranslation(CGPoint.zero, in: self)
82    storeState()
83  }
84  
85  @objc
86  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
87    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
88    sender.scale = 1
89    storeState()
90  }
91  
92  @objc
93  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
94    rotation += sender.rotation
95    transform = transform.rotated(by: sender.rotation)
96    sender.rotation = 0
97    storeState()
98  }
99  
100  // MARK: - Miscelaneous
101  
102  func moveToFront() {
103    superview?.bringSubviewToFront(self)
104  }
105  
106  public func rotated(by degrees: CGFloat) {
107    transform = transform.rotated(by: degrees)
108    rotation += degrees
109  }
110  
111  func storeState() {
112    print(&quot;&quot;&quot;
113    Element Frame = \(frame)
114    Element Bounds = \(bounds)
115    Element Center = \(center)
116    &quot;&quot;&quot;)
117    baseFrame = frame
118  }
119}
120let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
121                aspectFittedFrame.size.height / canvas.frame.height)
122let scale
123    = min(aspectFittedFrame.size.width / sourceRectangleSize.width,
124          aspectFittedFrame.size.height / sourceRectangleSize.height)
125

UIView translate scale rotate aspect ratio Swift ios

2. Is the scale being applied on the right values?

If you notice, the first order from 1:1 to 16:9 works quite well. However after that it does not seem to work and I believe the issue is here:

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38final class CanvasElement: UIView {
39  
40  var rotation: CGFloat = 0
41  var baseFrame: CGRect = .zero
42
43  var id: String = UUID().uuidString
44  
45  // MARK: - Initialization
46  
47  override init(frame: CGRect) {
48    super.init(frame: frame)
49    storeState()
50    setupGesture()
51  }
52  
53  required init?(coder aDecoder: NSCoder) {
54    super.init(coder: aDecoder)
55  }
56  
57  // MARK: - Gesture Setup
58  
59  private func setupGesture() {
60    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
61    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
62    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
63    addGestureRecognizer(panGestureRecognizer)
64    addGestureRecognizer(pinchGestureRecognizer)
65    addGestureRecognizer(rotateGestureRecognizer)
66  }
67  
68  // MARK: - Touches
69  
70  override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
71    super.touchesBegan(touches, with: event)
72    moveToFront()
73  }
74  
75  //MARK: - Gestures
76  
77  @objc
78  private func panGesture(_ sender: UIPanGestureRecognizer) {
79    let move = sender.translation(in: self)
80    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
81    sender.setTranslation(CGPoint.zero, in: self)
82    storeState()
83  }
84  
85  @objc
86  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
87    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
88    sender.scale = 1
89    storeState()
90  }
91  
92  @objc
93  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
94    rotation += sender.rotation
95    transform = transform.rotated(by: sender.rotation)
96    sender.rotation = 0
97    storeState()
98  }
99  
100  // MARK: - Miscelaneous
101  
102  func moveToFront() {
103    superview?.bringSubviewToFront(self)
104  }
105  
106  public func rotated(by degrees: CGFloat) {
107    transform = transform.rotated(by: degrees)
108    rotation += degrees
109  }
110  
111  func storeState() {
112    print(&quot;&quot;&quot;
113    Element Frame = \(frame)
114    Element Bounds = \(bounds)
115    Element Center = \(center)
116    &quot;&quot;&quot;)
117    baseFrame = frame
118  }
119}
120let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
121                aspectFittedFrame.size.height / canvas.frame.height)
122let scale
123    = min(aspectFittedFrame.size.width / sourceRectangleSize.width,
124          aspectFittedFrame.size.height / sourceRectangleSize.height)
125for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
126{
127    canvasElement.frame.size = CGSize(
128        width: canvasElement.baseFrame.width * scale,
129        height: canvasElement.baseFrame.height * scale
130    )
131    
132    canvasElement.frame.origin = CGPoint(
133        x: aspectFittedFrame.origin.x
134            + canvasElement.baseFrame.origin.x * scale,
135        
136        y:  aspectFittedFrame.origin.y
137            + canvasElement.baseFrame.origin.y * scale
138    )
139}
140

The first time, the scale works well because canvas and the canvas elements are being scaled in sync or mapped properly:

Swift ios UIView scaling transformation rotation translation

However, if you go beyond that, because you are always scaling based on the base values your aspect ratio frame and your canvas elements are out of sync

UIView scaling transformation rotation translation Swift ios

So in the example of 1:1 -> 16:9 -> 3:2

  • Your viewport has been scaled twice 1:1 -> 16:9 and from 16:9 -> 3:2
  • Whereas your elements are scaled once each time, 1:1 -> 16:9 and 1:1 -> 3:2 because you always scale from the base values

So I feel to see the values within the red viewport, you need to apply the same continuous scaling based on the previous view rather than the base view.

Just for an immediate quick fix, I update the base values of the canvas element after each change in canvas size by calling canvasElement.storeState():

1let sourceRectangleSize = canvas.frame.size
2
3canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
4view.layoutIfNeeded()
5
6let destinationRectangleSize = canvas.frame.size
7
8let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
9ratioVisualizer.frame = aspectFittedFrame
10@objc
11private func setRatio(_ control: UISegmentedControl) {
12  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
13  
14  let sourceRectangleSize = canvas.frame.size
15 
16  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
17  view.layoutIfNeeded()
18 
19  let destinationRectangleSize = canvas.frame.size
20  
21  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
22  ratioVisualizer.frame = aspectFittedFrame
23  
24  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
25  
26  for case let canvasElement as CanvasElement in canvas.subviews {
27  
28    canvasElement.frame.size = CGSize(
29      width: canvasElement.baseFrame.width * scale,
30      height: canvasElement.baseFrame.height * scale
31    )
32    canvasElement.frame.origin = CGPoint(
33      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
34      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
35    )
36  }
37}
38final class CanvasElement: UIView {
39  
40  var rotation: CGFloat = 0
41  var baseFrame: CGRect = .zero
42
43  var id: String = UUID().uuidString
44  
45  // MARK: - Initialization
46  
47  override init(frame: CGRect) {
48    super.init(frame: frame)
49    storeState()
50    setupGesture()
51  }
52  
53  required init?(coder aDecoder: NSCoder) {
54    super.init(coder: aDecoder)
55  }
56  
57  // MARK: - Gesture Setup
58  
59  private func setupGesture() {
60    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
61    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
62    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
63    addGestureRecognizer(panGestureRecognizer)
64    addGestureRecognizer(pinchGestureRecognizer)
65    addGestureRecognizer(rotateGestureRecognizer)
66  }
67  
68  // MARK: - Touches
69  
70  override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
71    super.touchesBegan(touches, with: event)
72    moveToFront()
73  }
74  
75  //MARK: - Gestures
76  
77  @objc
78  private func panGesture(_ sender: UIPanGestureRecognizer) {
79    let move = sender.translation(in: self)
80    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
81    sender.setTranslation(CGPoint.zero, in: self)
82    storeState()
83  }
84  
85  @objc
86  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
87    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
88    sender.scale = 1
89    storeState()
90  }
91  
92  @objc
93  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
94    rotation += sender.rotation
95    transform = transform.rotated(by: sender.rotation)
96    sender.rotation = 0
97    storeState()
98  }
99  
100  // MARK: - Miscelaneous
101  
102  func moveToFront() {
103    superview?.bringSubviewToFront(self)
104  }
105  
106  public func rotated(by degrees: CGFloat) {
107    transform = transform.rotated(by: degrees)
108    rotation += degrees
109  }
110  
111  func storeState() {
112    print(&quot;&quot;&quot;
113    Element Frame = \(frame)
114    Element Bounds = \(bounds)
115    Element Center = \(center)
116    &quot;&quot;&quot;)
117    baseFrame = frame
118  }
119}
120let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
121                aspectFittedFrame.size.height / canvas.frame.height)
122let scale
123    = min(aspectFittedFrame.size.width / sourceRectangleSize.width,
124          aspectFittedFrame.size.height / sourceRectangleSize.height)
125for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
126{
127    canvasElement.frame.size = CGSize(
128        width: canvasElement.baseFrame.width * scale,
129        height: canvasElement.baseFrame.height * scale
130    )
131    
132    canvasElement.frame.origin = CGPoint(
133        x: aspectFittedFrame.origin.x
134            + canvasElement.baseFrame.origin.x * scale,
135        
136        y:  aspectFittedFrame.origin.y
137            + canvasElement.baseFrame.origin.y * scale
138    )
139}
140for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
141{
142    canvasElement.frame.size = CGSize(
143        width: canvasElement.baseFrame.width * scale,
144        height: canvasElement.baseFrame.height * scale
145    )
146    
147    canvasElement.frame.origin = CGPoint(
148        x: aspectFittedFrame.origin.x
149            + canvasElement.baseFrame.origin.x * scale,
150        
151        y:  aspectFittedFrame.origin.y
152            + canvasElement.baseFrame.origin.y * scale
153    )
154    
155    // I added this
156    canvasElement.storeState()
157}
158

The result is perhaps closer to what you want ?

UIView Scaling Translation Rotation Aspect Ratio Swift iOS

Final thoughts

While this might fix your issue, you will notice that it is not possible to come back to the original state as at each step a transformation is applied.

A solution could be to store the current values mapped to a specific viewport aspect ratio and calculate the right sizes for the others so that if you needed to get back to the original, you could do that.

Source https://stackoverflow.com/questions/71004029

QUESTION

Prevent y-axis labels from being cut off

Asked 2022-Jan-31 at 04:00

My chart y labels are cut off and by trying different solution found on stackoverflow like adding spaces in labels or setting layout padding did not solved the problem.

The code

1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});
1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});.card-chart {
81  overflow: hidden;
82}
83
84.card {
85  display: flex;
86  flex-direction: column;
87  min-width: 0;
88  word-wrap: break-word;
89  background-color: #fff;
90  background-clip: border-box;
91  border: 0.0625rem solid rgba(34, 42, 66, .05);
92  border-radius: 0.2857rem;
93}
94
95.card {
96  background: #27293d;
97  border: 0;
98  position: relative;
99  width: 100%;
100  margin-bottom: 30px;
101  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
102}
103
104.card .card-body {
105  padding: 15px 15px 15px 15px;
106}
107
108.card-body {
109  flex: 1 1 auto;
110  padding: 1.5rem;
111}
112
113.card-chart .chart-area {
114  height: 220px;
115  width: calc(100% + 30px);
116}
1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});.card-chart {
81  overflow: hidden;
82}
83
84.card {
85  display: flex;
86  flex-direction: column;
87  min-width: 0;
88  word-wrap: break-word;
89  background-color: #fff;
90  background-clip: border-box;
91  border: 0.0625rem solid rgba(34, 42, 66, .05);
92  border-radius: 0.2857rem;
93}
94
95.card {
96  background: #27293d;
97  border: 0;
98  position: relative;
99  width: 100%;
100  margin-bottom: 30px;
101  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
102}
103
104.card .card-body {
105  padding: 15px 15px 15px 15px;
106}
107
108.card-body {
109  flex: 1 1 auto;
110  padding: 1.5rem;
111}
112
113.card-chart .chart-area {
114  height: 220px;
115  width: calc(100% + 30px);
116}&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
117&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"&gt;&lt;/script&gt;
118&lt;div class="card card-chart"&gt;
119  &lt;div class="card-header"&gt;
120    &lt;div class="row"&gt;
121      &lt;div class="col-sm-6 text-left"&gt;
122        &lt;h5 class="card-category"&gt;Totale vendite&lt;/h5&gt;
123        &lt;h2 class="card-title"&gt;Totali&lt;/h2&gt;
124      &lt;/div&gt;
125    &lt;/div&gt;
126  &lt;/div&gt;
127  &lt;div class="card-body"&gt;
128    &lt;div class="chart-area"&gt;
129      &lt;canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"&gt;&lt;/canvas&gt;
130    &lt;/div&gt;
131  &lt;/div&gt;
132&lt;/div&gt;

ANSWER

Answered 2022-Jan-26 at 16:52

The sampleSize property in your y axis config is the culprit, since you put it to 1 it only looks at the first tick for the length that it can use. But other data in your array is way larger so it wont fit. Removing this property or making it a bigger number so it would sample more ticks will resolve your behaviour (removing will give most consistent results).

1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});.card-chart {
81  overflow: hidden;
82}
83
84.card {
85  display: flex;
86  flex-direction: column;
87  min-width: 0;
88  word-wrap: break-word;
89  background-color: #fff;
90  background-clip: border-box;
91  border: 0.0625rem solid rgba(34, 42, 66, .05);
92  border-radius: 0.2857rem;
93}
94
95.card {
96  background: #27293d;
97  border: 0;
98  position: relative;
99  width: 100%;
100  margin-bottom: 30px;
101  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
102}
103
104.card .card-body {
105  padding: 15px 15px 15px 15px;
106}
107
108.card-body {
109  flex: 1 1 auto;
110  padding: 1.5rem;
111}
112
113.card-chart .chart-area {
114  height: 220px;
115  width: calc(100% + 30px);
116}&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
117&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"&gt;&lt;/script&gt;
118&lt;div class="card card-chart"&gt;
119  &lt;div class="card-header"&gt;
120    &lt;div class="row"&gt;
121      &lt;div class="col-sm-6 text-left"&gt;
122        &lt;h5 class="card-category"&gt;Totale vendite&lt;/h5&gt;
123        &lt;h2 class="card-title"&gt;Totali&lt;/h2&gt;
124      &lt;/div&gt;
125    &lt;/div&gt;
126  &lt;/div&gt;
127  &lt;div class="card-body"&gt;
128    &lt;div class="chart-area"&gt;
129      &lt;canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"&gt;&lt;/canvas&gt;
130    &lt;/div&gt;
131  &lt;/div&gt;
132&lt;/div&gt;const optionsTotali = {
133  maintainAspectRatio: false,
134  responsive: true,
135  plugins: {
136    legend: {
137      display: false
138    },
139    tooltip: {
140      displayColors: false,
141      mode: "index",
142      intersect: 0,
143      callbacks: {
144        label: function(context) {
145          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
146        }
147      }
148    },
149  },
150  scales: {
151    y: {
152      grid: {
153        display: false
154      },
155      ticks: {
156        min: 0,
157        beginAtZero: true,
158        callback: function(value, index, values) {
159          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
160        }
161      }
162    }
163  }
164};
165
166const ctx = document.getElementById("chartTotali").getContext('2d');
167const chartTotali = new Chart(ctx, {
168  type: 'line',
169  data: {
170    labels: [
171      "08:00",
172      "09:00",
173      "10:00",
174      "11:00",
175      "12:00",
176      "13:00",
177      "14:00",
178      "15:00",
179      "16:00",
180      "17:00",
181      "18:00",
182      "19:00",
183      "20:00"
184    ],
185    datasets: [{
186      label: "Totale €",
187      fill: true,
188      backgroundColor: '#0084ff',
189      borderColor: '#0084ff',
190      borderWidth: 2,
191      pointBackgroundColor: '#0084ff',
192      data: [
193        "17089.36",
194        "394279.52",
195        "514863.02",
196        "540198.74",
197        "379222.06",
198        "8793.42",
199        "79.58",
200        "116379.41",
201        "444580.43",
202        "506663.36",
203        "457947.28",
204        "138158.94",
205        "398.46"
206      ],
207    }]
208  },
209  options: optionsTotali
210});
1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});.card-chart {
81  overflow: hidden;
82}
83
84.card {
85  display: flex;
86  flex-direction: column;
87  min-width: 0;
88  word-wrap: break-word;
89  background-color: #fff;
90  background-clip: border-box;
91  border: 0.0625rem solid rgba(34, 42, 66, .05);
92  border-radius: 0.2857rem;
93}
94
95.card {
96  background: #27293d;
97  border: 0;
98  position: relative;
99  width: 100%;
100  margin-bottom: 30px;
101  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
102}
103
104.card .card-body {
105  padding: 15px 15px 15px 15px;
106}
107
108.card-body {
109  flex: 1 1 auto;
110  padding: 1.5rem;
111}
112
113.card-chart .chart-area {
114  height: 220px;
115  width: calc(100% + 30px);
116}&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
117&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"&gt;&lt;/script&gt;
118&lt;div class="card card-chart"&gt;
119  &lt;div class="card-header"&gt;
120    &lt;div class="row"&gt;
121      &lt;div class="col-sm-6 text-left"&gt;
122        &lt;h5 class="card-category"&gt;Totale vendite&lt;/h5&gt;
123        &lt;h2 class="card-title"&gt;Totali&lt;/h2&gt;
124      &lt;/div&gt;
125    &lt;/div&gt;
126  &lt;/div&gt;
127  &lt;div class="card-body"&gt;
128    &lt;div class="chart-area"&gt;
129      &lt;canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"&gt;&lt;/canvas&gt;
130    &lt;/div&gt;
131  &lt;/div&gt;
132&lt;/div&gt;const optionsTotali = {
133  maintainAspectRatio: false,
134  responsive: true,
135  plugins: {
136    legend: {
137      display: false
138    },
139    tooltip: {
140      displayColors: false,
141      mode: "index",
142      intersect: 0,
143      callbacks: {
144        label: function(context) {
145          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
146        }
147      }
148    },
149  },
150  scales: {
151    y: {
152      grid: {
153        display: false
154      },
155      ticks: {
156        min: 0,
157        beginAtZero: true,
158        callback: function(value, index, values) {
159          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
160        }
161      }
162    }
163  }
164};
165
166const ctx = document.getElementById("chartTotali").getContext('2d');
167const chartTotali = new Chart(ctx, {
168  type: 'line',
169  data: {
170    labels: [
171      "08:00",
172      "09:00",
173      "10:00",
174      "11:00",
175      "12:00",
176      "13:00",
177      "14:00",
178      "15:00",
179      "16:00",
180      "17:00",
181      "18:00",
182      "19:00",
183      "20:00"
184    ],
185    datasets: [{
186      label: "Totale €",
187      fill: true,
188      backgroundColor: '#0084ff',
189      borderColor: '#0084ff',
190      borderWidth: 2,
191      pointBackgroundColor: '#0084ff',
192      data: [
193        "17089.36",
194        "394279.52",
195        "514863.02",
196        "540198.74",
197        "379222.06",
198        "8793.42",
199        "79.58",
200        "116379.41",
201        "444580.43",
202        "506663.36",
203        "457947.28",
204        "138158.94",
205        "398.46"
206      ],
207    }]
208  },
209  options: optionsTotali
210});.card-chart {
211  overflow: hidden;
212}
213
214.card {
215  display: flex;
216  flex-direction: column;
217  min-width: 0;
218  word-wrap: break-word;
219  background-color: #fff;
220  background-clip: border-box;
221  border: 0.0625rem solid rgba(34, 42, 66, .05);
222  border-radius: 0.2857rem;
223}
224
225.card {
226  background: #27293d;
227  border: 0;
228  position: relative;
229  width: 100%;
230  margin-bottom: 30px;
231  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
232}
233
234.card .card-body {
235  padding: 15px 15px 15px 15px;
236}
237
238.card-body {
239  flex: 1 1 auto;
240  padding: 1.5rem;
241}
242
243.card-chart .chart-area {
244  height: 220px;
245  width: calc(100% + 30px);
246}
1const optionsTotali = {
2  maintainAspectRatio: false,
3  responsive: true,
4  plugins: {
5    legend: {
6      display: false
7    },
8    tooltip: {
9      displayColors: false,
10      mode: "index",
11      intersect: 0,
12      callbacks: {
13        label: function(context) {
14          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
15        }
16      }
17    },
18  },
19  scales: {
20    y: {
21      grid: {
22        display: false
23      },
24      ticks: {
25        min: 0,
26        beginAtZero: true,
27        sampleSize: 1,
28        callback: function(value, index, values) {
29          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
30        }
31      }
32    }
33  }
34};
35
36const ctx = document.getElementById("chartTotali").getContext('2d');
37const chartTotali = new Chart(ctx, {
38  type: 'line',
39  data: {
40    labels: [
41      "08:00",
42      "09:00",
43      "10:00",
44      "11:00",
45      "12:00",
46      "13:00",
47      "14:00",
48      "15:00",
49      "16:00",
50      "17:00",
51      "18:00",
52      "19:00",
53      "20:00"
54    ],
55    datasets: [{
56      label: "Totale €",
57      fill: true,
58      backgroundColor: '#0084ff',
59      borderColor: '#0084ff',
60      borderWidth: 2,
61      pointBackgroundColor: '#0084ff',
62      data: [
63        "17089.36",
64        "394279.52",
65        "514863.02",
66        "540198.74",
67        "379222.06",
68        "8793.42",
69        "79.58",
70        "116379.41",
71        "444580.43",
72        "506663.36",
73        "457947.28",
74        "138158.94",
75        "398.46"
76      ],
77    }]
78  },
79  options: optionsTotali
80});.card-chart {
81  overflow: hidden;
82}
83
84.card {
85  display: flex;
86  flex-direction: column;
87  min-width: 0;
88  word-wrap: break-word;
89  background-color: #fff;
90  background-clip: border-box;
91  border: 0.0625rem solid rgba(34, 42, 66, .05);
92  border-radius: 0.2857rem;
93}
94
95.card {
96  background: #27293d;
97  border: 0;
98  position: relative;
99  width: 100%;
100  margin-bottom: 30px;
101  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
102}
103
104.card .card-body {
105  padding: 15px 15px 15px 15px;
106}
107
108.card-body {
109  flex: 1 1 auto;
110  padding: 1.5rem;
111}
112
113.card-chart .chart-area {
114  height: 220px;
115  width: calc(100% + 30px);
116}&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
117&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"&gt;&lt;/script&gt;
118&lt;div class="card card-chart"&gt;
119  &lt;div class="card-header"&gt;
120    &lt;div class="row"&gt;
121      &lt;div class="col-sm-6 text-left"&gt;
122        &lt;h5 class="card-category"&gt;Totale vendite&lt;/h5&gt;
123        &lt;h2 class="card-title"&gt;Totali&lt;/h2&gt;
124      &lt;/div&gt;
125    &lt;/div&gt;
126  &lt;/div&gt;
127  &lt;div class="card-body"&gt;
128    &lt;div class="chart-area"&gt;
129      &lt;canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"&gt;&lt;/canvas&gt;
130    &lt;/div&gt;
131  &lt;/div&gt;
132&lt;/div&gt;const optionsTotali = {
133  maintainAspectRatio: false,
134  responsive: true,
135  plugins: {
136    legend: {
137      display: false
138    },
139    tooltip: {
140      displayColors: false,
141      mode: "index",
142      intersect: 0,
143      callbacks: {
144        label: function(context) {
145          return "€" + context.parsed.y.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&amp;,').replace(/[,.]/g, m =&gt; (m === ',' ? '.' : ','));
146        }
147      }
148    },
149  },
150  scales: {
151    y: {
152      grid: {
153        display: false
154      },
155      ticks: {
156        min: 0,
157        beginAtZero: true,
158        callback: function(value, index, values) {
159          return "€" + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
160        }
161      }
162    }
163  }
164};
165
166const ctx = document.getElementById("chartTotali").getContext('2d');
167const chartTotali = new Chart(ctx, {
168  type: 'line',
169  data: {
170    labels: [
171      "08:00",
172      "09:00",
173      "10:00",
174      "11:00",
175      "12:00",
176      "13:00",
177      "14:00",
178      "15:00",
179      "16:00",
180      "17:00",
181      "18:00",
182      "19:00",
183      "20:00"
184    ],
185    datasets: [{
186      label: "Totale €",
187      fill: true,
188      backgroundColor: '#0084ff',
189      borderColor: '#0084ff',
190      borderWidth: 2,
191      pointBackgroundColor: '#0084ff',
192      data: [
193        "17089.36",
194        "394279.52",
195        "514863.02",
196        "540198.74",
197        "379222.06",
198        "8793.42",
199        "79.58",
200        "116379.41",
201        "444580.43",
202        "506663.36",
203        "457947.28",
204        "138158.94",
205        "398.46"
206      ],
207    }]
208  },
209  options: optionsTotali
210});.card-chart {
211  overflow: hidden;
212}
213
214.card {
215  display: flex;
216  flex-direction: column;
217  min-width: 0;
218  word-wrap: break-word;
219  background-color: #fff;
220  background-clip: border-box;
221  border: 0.0625rem solid rgba(34, 42, 66, .05);
222  border-radius: 0.2857rem;
223}
224
225.card {
226  background: #27293d;
227  border: 0;
228  position: relative;
229  width: 100%;
230  margin-bottom: 30px;
231  box-shadow: 0 1px 20px 0 rgb(0 0 0 / 10%);
232}
233
234.card .card-body {
235  padding: 15px 15px 15px 15px;
236}
237
238.card-body {
239  flex: 1 1 auto;
240  padding: 1.5rem;
241}
242
243.card-chart .chart-area {
244  height: 220px;
245  width: calc(100% + 30px);
246}&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"&gt;
247&lt;script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"&gt;&lt;/script&gt;
248&lt;div class="card card-chart"&gt;
249  &lt;div class="card-header"&gt;
250    &lt;div class="row"&gt;
251      &lt;div class="col-sm-6 text-left"&gt;
252        &lt;h5 class="card-category"&gt;Totale vendite&lt;/h5&gt;
253        &lt;h2 class="card-title"&gt;Totali&lt;/h2&gt;
254      &lt;/div&gt;
255    &lt;/div&gt;
256  &lt;/div&gt;
257  &lt;div class="card-body"&gt;
258    &lt;div class="chart-area"&gt;
259      &lt;canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"&gt;&lt;/canvas&gt;
260    &lt;/div&gt;
261  &lt;/div&gt;
262&lt;/div&gt;

Source https://stackoverflow.com/questions/70832600

QUESTION

Javascript: frame precise video stop

Asked 2022-Jan-28 at 14:55

I would like to be able to robustly stop a video when the video arrives on some specified frames in order to do oral presentations based on videos made with Blender, Manim...

I'm aware of this question, but the problem is that the video does not stops exactly at the good frame. Sometimes it continues forward for one frame and when I force it to come back to the initial frame we see the video going backward, which is weird. Even worse, if the next frame is completely different (different background...) this will be very visible.

To illustrate my issues, I created a demo project here (just click "next" and see that when the video stops, sometimes it goes backward). The full code is here.

The important part of the code I'm using is:

1      var video = VideoFrame({
2          id: 'video',
3          frameRate: 24,
4          callback: function(curr_frame) {
5              // Stops the video when arriving on a frames to stop at.
6              if (stopFrames.includes(curr_frame)) {
7                  console.log(&quot;Automatic stop: found stop frame.&quot;);
8                  pauseMyVideo();
9                  // Ensure we are on the proper frame.
10                  video.seekTo({frame: curr_frame});
11              }
12          }
13      });
14

So far, I avoid this issue by stopping one frame before the end and then using seekTo (not sure how sound this is), as demonstrated here. But as you can see, sometimes when going on the next frame it "freezes" a bit: I guess this is when the stop happens right before the seekTo.

PS: if you know a reliable way in JS to know the number of frames of a given video, I'm also interested.

Concerning the idea to cut the video before hand on the desktop, this could be used... but I had bad experience with that in the past, notably as changing videos sometimes produce some glitches. Also, it can be more complicated to use at it means that the video should be manually cut a lot of time, re-encoded...

EDIT Is there any solution for instance based on WebAssembly (more compatible with old browsers) or Webcodec (more efficient, but not yet wide-spread)? Webcodec seems to allow pretty amazing things, but I'm not sure how to use them for that. I would love to hear solution based on both of them since firefox does not handle webcodec yet. Note that it would be great if audio is not lost in the process. Bonus if I can also make controls appear on request.

EDIT: I'm not sure to understand what's happening here (source)... But it seems to do something close to my need (using webassembly I think) since it manages to play a video in a canvas, with frame... Here is another website that does something close to my need using Webcodec. But I'm not sure how to reliably synchronize sound and video with webcodec.

EDIT: answer to the first question

Concerning the video frame, indeed I chose poorly my frame rate, it was 25 not 24. But even by using a framerate of 25, I still don't get a frame-precise stop, on both Firefox and Chromium. For instance, here is a recording (using OBS) of your demo (I see the same with mine when I use 25 instead of 24):

enter image description here

one frame later, see that the butter "fly backward"(this is maybe not very visible with still screenshots, but see for instance the position of the lower left wing in the flowers):

enter image description here

I can see three potential reasons: first (I think it is the most likely reason), I heard that video.currentTime was not always reporting accurately the time, maybe it could explain why here it fails? It seems to be pretty accurate in order to change the current frame (I can go forward and backward by one frame quite reliably as far as I can see), but people reported here that video.currentTime is computed using the audio time and not the video time in Chromium, leading to some inconsistencies (I observe similar inconsistencies in Firefox), and here that it may either lead the time at which the frame is sent to the compositor or at which the frame is actually printed in the compositor (if it is the latest, it could explain the delay we have sometimes). This would also explain why requestAnimationVideoFrame is better, as it also provides the current media time.

The second reason that could explain that problem is that setInterval may not be precise enough... However requestAnimationFrame is not really better (requestAnimationVideoFrame is not available in Firefox) while it should fire 60 times per seconds which should be quick enough.

The third option I can see is that maybe the .pause function is quite long to fire... and that by the end of the call the video also plays another frame. On the other hand, your example using requestAnimationVideoFrame https://mvyom.csb.app/requestFrame.html seems to work pretty reliably, and it's using .pause! Unfortunately it only works in Chromium, but not in firefox. I see that you use metadata.mediaTime instead of currentTime, maybe this is more precise than current time.

The last option is that there is maybe something subtle concerning vsync as explained in this page. It also reports that expectedDisplayTime may help to solve this issue when using requestAnimationVideoFrame.

ANSWER

Answered 2022-Jan-21 at 19:18

The video has frame rate of 25fps, and not 24fps: enter image description here

After putting the correct value it works ok: demo
The VideoFrame api heavily relies on FPS provided by you. You can find FPS of your videos offline and send as metadata along with stop frames from server.


The site videoplayer.handmadeproductions.de uses window.requestAnimationFrame() to get the callback.


There is a new better alternative to requestAnimationFrame. The requestVideoFrameCallback(), allows us to do per-video-frame operations on video.
The same functionality, you domed in OP, can be achieved like this:

1      var video = VideoFrame({
2          id: 'video',
3          frameRate: 24,
4          callback: function(curr_frame) {
5              // Stops the video when arriving on a frames to stop at.
6              if (stopFrames.includes(curr_frame)) {
7                  console.log(&quot;Automatic stop: found stop frame.&quot;);
8                  pauseMyVideo();
9                  // Ensure we are on the proper frame.
10                  video.seekTo({frame: curr_frame});
11              }
12          }
13      });
14   const callback = (now, metadata) =&gt; {
15      if (startTime == 0) {
16        startTime = now;
17      }
18      elapsed = metadata.mediaTime;
19      currentFrame = metadata.presentedFrames - doneCount;
20
21      fps = (currentFrame / elapsed).toFixed(3);
22      fps = !isFinite(fps) ? 0 : fps;
23
24      updateStats();
25      if (stopFrames.includes(currentFrame)) {
26        pauseMyVideo();
27      } else {
28        video.requestVideoFrameCallback(callback);
29      }
30   };
31   video.requestVideoFrameCallback(callback);
32

And here is how demo looks like.
The API works on chromium based browsers like Chrome, Edge, Brave etc.


There is a JS library, which finds frame rate from video binary file, named mediainfo.js.

Source https://stackoverflow.com/questions/70613008

QUESTION

Opening PDFs in WebView2 based on selection in CheckBoxColumn

Asked 2022-Jan-19 at 14:26

So,

In my WPF application, I want my users to be able to open previews of invoices, so that they may either verify or discard them. I am letting them check rows (each row representing a invoice) in a DataGridCheckBoxColumn in my DataGrid, then clicking a button (which runs my CreateInvoicePreview() method, see bottom of post), having all of the invoice previews be opened in new windows (one window for each invoice).

Well.. What happens now, is: User checks InvoiceA and InvoiceB. Two invoices are opened, but they are the same: InvoiceC. The correct amount of invoices are always opened, but not the correct instance. If I open the temp folder specified in my file path, I see that all invoices in the datagrid has been saved: InvoiceA through InvoiceJ.

Let me take you through the code.

This is the method that creates that builds and saves the actual PDF's, which the WebView2 control uses as source, so that it can display them in-app. It is heavily abbreviated. I have kept the structure with the nested foreach loops in case that is relevant.

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51

As you see, towards the end of the method I have commented out a bit of code, which was me trying to implement a way to filter based on the checked rows only. It did not work.

This is the XAML for the WebView2:

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51&lt;Wpf:WebView2 
52    x:Name=&quot;wv_preview_invoice&quot; Loaded=&quot;{s:Action CreatePreviewInvoice}&quot; 
53    Height=&quot;997&quot; Width=&quot;702&quot; Canvas.Left=&quot;20&quot; Canvas.Top=&quot;71&quot; 
54    Source=&quot;{Binding PdfFilePath}&quot;&gt;&lt;/Wpf:WebView2&gt;
55

PdfFilePath is a property, which is referenced within the method above. It's given a value within the method, and when Source (for the WebView2) is called, it is able to get the value from PdfFilePath, and thus display the PDF.

But as I said initially, it just creates X amount of instances/windows of the same invoice. Always the same one, because of in what order they are queried from the database.

And finally, here is the method that run when they click whichever invoices they want to preview, it's to open the new window with the WebView2 control:

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51&lt;Wpf:WebView2 
52    x:Name=&quot;wv_preview_invoice&quot; Loaded=&quot;{s:Action CreatePreviewInvoice}&quot; 
53    Height=&quot;997&quot; Width=&quot;702&quot; Canvas.Left=&quot;20&quot; Canvas.Top=&quot;71&quot; 
54    Source=&quot;{Binding PdfFilePath}&quot;&gt;&lt;/Wpf:WebView2&gt;
55public void PreviewInvoices() {
56
57    bool success = false;
58
59    foreach (PaidTrip item in PaidTrips) {
60
61        if (item.IsChecked == true) {
62
63            ShowPreviewInvoiceDetailed(item);
64            success = true;
65        }
66    }
67
68    if (!success) {
69
70        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
71    }
72}
73

The method that opens the next window where the WebView2 is, looks like this:

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51&lt;Wpf:WebView2 
52    x:Name=&quot;wv_preview_invoice&quot; Loaded=&quot;{s:Action CreatePreviewInvoice}&quot; 
53    Height=&quot;997&quot; Width=&quot;702&quot; Canvas.Left=&quot;20&quot; Canvas.Top=&quot;71&quot; 
54    Source=&quot;{Binding PdfFilePath}&quot;&gt;&lt;/Wpf:WebView2&gt;
55public void PreviewInvoices() {
56
57    bool success = false;
58
59    foreach (PaidTrip item in PaidTrips) {
60
61        if (item.IsChecked == true) {
62
63            ShowPreviewInvoiceDetailed(item);
64            success = true;
65        }
66    }
67
68    if (!success) {
69
70        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
71    }
72}
73 public void ShowPreviewInvoiceDetailed() {
74
75    PreviewInvoiceDetailedViewModel viewModel = new(windowManager);
76    windowManager.ShowWindow(viewModel);
77}
78

What part (or several parts) of the picture am I missing?

ANSWER

Answered 2022-Jan-19 at 14:26

I managed to solve this by doing the following:

I made a property; public static string PreviewedInvoice { get; set; } in the ViewModel of the parent window. In my method that opens the child window (where the preview invoices are to be displayed) I bind it to LicenseHolderID of the rows that have a checked CheckBox, via foreach loop, like such:

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51&lt;Wpf:WebView2 
52    x:Name=&quot;wv_preview_invoice&quot; Loaded=&quot;{s:Action CreatePreviewInvoice}&quot; 
53    Height=&quot;997&quot; Width=&quot;702&quot; Canvas.Left=&quot;20&quot; Canvas.Top=&quot;71&quot; 
54    Source=&quot;{Binding PdfFilePath}&quot;&gt;&lt;/Wpf:WebView2&gt;
55public void PreviewInvoices() {
56
57    bool success = false;
58
59    foreach (PaidTrip item in PaidTrips) {
60
61        if (item.IsChecked == true) {
62
63            ShowPreviewInvoiceDetailed(item);
64            success = true;
65        }
66    }
67
68    if (!success) {
69
70        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
71    }
72}
73 public void ShowPreviewInvoiceDetailed() {
74
75    PreviewInvoiceDetailedViewModel viewModel = new(windowManager);
76    windowManager.ShowWindow(viewModel);
77}
78public void PreviewInvoices() {
79
80    bool success = false;
81
82    foreach (PaidTrip item in PaidTrips) {
83
84        if (item.IsChecked == true) {
85
86            PreviewedInvoice = item.LicenseHolderID;
87            ShowPreviewInvoiceDetailed();
88            success = true;
89        }
90    }
91
92    if (!success) {
93
94        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
95    }
96}
97
98

So now I have a way to filter only the checked rows, and make sure only the LicenseHolderID which match those in the row with a checked CheckBox, are actually saved. I updated my main method:

1public void CreatePreviewInvoice() {
2
3    /* SQL SERVER CODE
4     * SQL SERVER CODE
5     * SQL SERVER CODE */
6
7    List&lt;PaidTrip&gt; paidTrips = PaidTrips.ToList();
8
9    tripsGroupedByCompany = paidTrips.GroupBy(pt =&gt; pt.LicenseHolderID);
10
11    foreach (IGrouping&lt;string, PaidTrip&gt; companyGroup in tripsGroupedByCompany) {
12
13        /* SQL SERVER CODE
14         * SQL SERVER CODE
15         * SQL SERVER CODE */
16
17        List&lt;LicenseHolder&gt; licenseHolders = LicenseHolders.ToList();
18
19        IEnumerable&lt;IGrouping&lt;string, PaidTrip&gt;&gt; groupedByVehicle = companyGroup.GroupBy(n =&gt; n.VehicleID);
20
21        foreach (IGrouping&lt;string, PaidTrip&gt; vehicleGroup in groupedByVehicle) {
22
23            // Iterating over all data pertaining to each vehicle
24            foreach (PaidTrip trip in vehicleGroup) {
25
26            }
27
28        try {
29
30            string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
31            string fileName = $&quot;FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + &quot;_&quot; + &quot;Faktura_&quot; + InvoiceID}.pdf&quot;;
32            string filePath = $@&quot;C:\Users\{userName}\AppData\Local\Temp\&quot;;
33
34            PdfFilePath = $&quot;{filePath}{fileName}&quot;;
35
36            //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38            document.Save($&quot;{PdfFilePath}&quot;);
39
40            //} else {
41
42            //    return;
43            //}                  
44
45        } catch (Exception ex) {
46
47            MessageBox.Show(ex.Message);
48        }
49    }
50}
51&lt;Wpf:WebView2 
52    x:Name=&quot;wv_preview_invoice&quot; Loaded=&quot;{s:Action CreatePreviewInvoice}&quot; 
53    Height=&quot;997&quot; Width=&quot;702&quot; Canvas.Left=&quot;20&quot; Canvas.Top=&quot;71&quot; 
54    Source=&quot;{Binding PdfFilePath}&quot;&gt;&lt;/Wpf:WebView2&gt;
55public void PreviewInvoices() {
56
57    bool success = false;
58
59    foreach (PaidTrip item in PaidTrips) {
60
61        if (item.IsChecked == true) {
62
63            ShowPreviewInvoiceDetailed(item);
64            success = true;
65        }
66    }
67
68    if (!success) {
69
70        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
71    }
72}
73 public void ShowPreviewInvoiceDetailed() {
74
75    PreviewInvoiceDetailedViewModel viewModel = new(windowManager);
76    windowManager.ShowWindow(viewModel);
77}
78public void PreviewInvoices() {
79
80    bool success = false;
81
82    foreach (PaidTrip item in PaidTrips) {
83
84        if (item.IsChecked == true) {
85
86            PreviewedInvoice = item.LicenseHolderID;
87            ShowPreviewInvoiceDetailed();
88            success = true;
89        }
90    }
91
92    if (!success) {
93
94        MessageBox.Show(&quot;You must chose an invoice to preview first.&quot;);
95    }
96}
97
98if (LicenseHolderID == PreviewInvoiceViewModel.PreviewedInvoice) {
99    document.Save($&quot;{fullPath}&quot;);
100    SourcePath = fullPath;
101}
102

And I bound SourcePath to the the source of the WebView2 in the XAML.

I feel like this is a clunky way of doing it, and I am going back and forth between layers, as a comment (since removed) mentioned.

If anyone can show me a better way, I'm all ears..

Source https://stackoverflow.com/questions/70711775

QUESTION

Is it possible to manually update the value of a Behaviour? (Functional Reactive Programming, Threepenny)

Asked 2022-Jan-17 at 16:02

I really hope I haven't gone down a dead-end here. I have a Behaviour that gives the currently selected Color, and the current mouse coordinates, then carries out a task when the mouse is clicked. That task involves looking at a list and then updating the values in that list, for it to be retrieved later. The fact that I can "store" the selected color gives me hope that storing a list can be done in a similar manner. I'm just at a dead end and not sure how to solve this. Would really appreciate some help.

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34

ANSWER

Answered 2022-Jan-17 at 16:02

Full credit to this response from duplode, I'll just go through how it was solved:

Let's say we have a function that modifies a list somehow, depending on some value. How/why updateMyList modifies the list doesn't really matter for this explanation, we just need to know its type. For this example, we'll say the value that determines how the list changes is a mouse coordinate tuple (x, y), which we'll pass as its first parameter:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36

If we have an Event that tells us the mouse coordinates when the user clicks:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords &lt;- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords &lt;@ UI.click canvas -- this is the Event we need
41

What we need to do is fmap the list-updating function onto mouseClicked:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords &lt;- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords &lt;@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42

So we've created a new Event: when mouseClicked is triggered, the mouse coordinates are passed as the first parameter to updateMyList, and that is the value of our new Event at that timestamp. But this is a partially applied function, updateMyList still requires an [Integer] as a parameter, so as a result, listChangeEvent has the following type:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords &lt;- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords &lt;@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -&gt; [Integer])
43

Now, this is the clever part: if we use accumB and specify the starting accumulator (i.e. our starting list, [1,2,3,4]), and then also use the above listChangeEvent as the Event accumB takes its value from:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords &lt;- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords &lt;@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -&gt; [Integer])
43listState      &lt;- accumB ([1,2,3,4]) listChangeEvent
44

Then that accumulator is what will be passed to the function in Event ([Integer] -> [Integer]). Meaning the first time the listChangeEvent triggers, updateMyList will be called with:

1-- There is a Blue button and a Red button on our UI. Whichever
2-- button was clicked last is our current color selection.
3colorRedSelected = const ColorRed &lt;$ UI.click redButton
4colorBlueSelected = const ColorBlue &lt;$ UI.click blueButton
5
6-- we combine both the above Events to create a new one that tells us the current selected color
7colorSelected = unionWith const colorRedSelected colorBlueSelected
8
9-- accumulate values for our Behaviour, starting with ColorRed selected by default
10colorMode       &lt;- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate   &lt;- stepper (0,0) $ UI.mousemove canvas
14
15-- want to start with the list [1,2,3,4], but this value should change later.
16-- I have 'never' here, as I don't know what else to put here yet.
17
18listState      &lt;- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) &lt;$&gt; colorMode &lt;*&gt; mouseCoordinate &lt;*&gt; listState
23
24-- Apply the event (of the user clicking the canvas) to the Behaviour,
25-- creating a new Event that returns the above tuple when it fires    
26
27makeChoice = choices &lt;@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -&gt; do    
30    ...
31    -- in this block we use the savedList, and generate a newList.
32    -- I want to update the choicePosition behaviour so that the newList
33    -- replaces the old savedList.
34updateMyList :: (Double, Double) -&gt; [Integer] -&gt; [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords &lt;- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords &lt;@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -&gt; [Integer])
43listState      &lt;- accumB ([1,2,3,4]) listChangeEvent
44updateMyList (x, y) [1,2,3,4] -- (x, y) being the mouse coordinates at that time
45

And the result of that becomes the new accumulator value in listState, and that new list will be used as the parameter to updateMyList the next time listChangeEventtriggers, and so on.

We can use this for anything at all, it doesn't necessarily have to be a list that we're modifying. This just gives us a way to initialize a Behavior with a value, and that we can specify exactly how the next value of the Behavior is derived, by creating a function that is equivalent to updateMyList.

Source https://stackoverflow.com/questions/70691558

QUESTION

Problem resizing plot on tkinter figure canvas

Asked 2022-Jan-15 at 02:30

Python 3.9 on Mac running OS 11.6.1. My application involves placing a plot on a frame inside my root window, and I'm struggling to get the plot to take up a larger portion of the window. I thought rcParams in matplotlib.pyplot would take care of this, but I must be overlooking something.

Here's what I have so far:

1import numpy as np
2from tkinter import Tk,Frame,TOP,BOTH
3
4import matplotlib
5from matplotlib import pyplot as plt
6from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
7
8plt.rcParams[&quot;figure.figsize&quot;] = [18,10]
9
10root=Tk()
11root.wm_title(&quot;Root Window&quot;)
12root.geometry('1500x1000')
13
14x = np.linspace(0, 2 * np.pi, 400)
15y = np.sin(x ** 2)
16fig, ax = plt.subplots()
17ax.plot(x, y)
18
19canvas_frame=Frame(root) # also tried adjusting size of frame but that didn't help
20canvas_frame.pack(side=TOP,expand=True)
21canvas = FigureCanvasTkAgg(fig, master=canvas_frame)
22canvas.draw()
23canvas.get_tk_widget().pack(side=TOP,fill=BOTH,expand=True)
24
25
26root.mainloop()
27

For my actual application, I need for canvas to have a frame as its parent and not simply root, which is why canvas_frame is introduced above.

ANSWER

Answered 2022-Jan-14 at 23:23

try something like this:

1import numpy as np
2from tkinter import Tk,Frame,TOP,BOTH
3
4import matplotlib
5from matplotlib import pyplot as plt
6from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
7
8plt.rcParams[&quot;figure.figsize&quot;] = [18,10]
9
10root=Tk()
11root.wm_title(&quot;Root Window&quot;)
12root.geometry('1500x1000')
13
14x = np.linspace(0, 2 * np.pi, 400)
15y = np.sin(x ** 2)
16fig, ax = plt.subplots()
17ax.plot(x, y)
18
19canvas_frame=Frame(root) # also tried adjusting size of frame but that didn't help
20canvas_frame.pack(side=TOP,expand=True)
21canvas = FigureCanvasTkAgg(fig, master=canvas_frame)
22canvas.draw()
23canvas.get_tk_widget().pack(side=TOP,fill=BOTH,expand=True)
24
25
26root.mainloop()
27fig.subplots_adjust(left=0.05, bottom=0.07, right=0.95, top=0.95, wspace=0, hspace=0)
28

this is output, figure now takes more screen area % [figure now takes more screen realestate1

Source https://stackoverflow.com/questions/70598913

QUESTION

Efficient code for custom color formatting in tkinter python

Asked 2022-Jan-11 at 14:31

[Editing this question completely] Thank you , for those who helped in building the Periodic Table successfully . As I completed it , I tried to link it with another of my project E-Search , which acts like Google and fetches answers , except that it will fetch me the data of the Periodic Table .

But , I got a problem - not with the searching but with the layout . I'm trying to layout the x-scrollbar in my canvas which will display results regarding the search . However , it is not properly done . Can anyone please help ?

Below here is my code :

1import tkinter as tk
2import PT
3
4root = tk.Tk()
5root.attributes('-fullscreen', True)
6root.config(bg='black')
7
8tk.Button(root, text='EXIT', command=root.destroy).place(x=0, y=0)
9
10elementals = PT.the_element_list
11
12search = tk.StringVar()
13
14search_entry = tk.Entry(root, textvariable=search)
15search_entry.place(relx=0.3, rely=0.3)
16
17
18def search_engine():
19
20    results = 0
21    var1 = 0
22
23    texts = [&quot;Name : &quot;, &quot;Atomic No. : &quot;, &quot;Atomic Mass : &quot;, &quot;Block : &quot;, &quot;Group No. : &quot;, &quot;Period No. : &quot;, &quot;Type : &quot;, &quot;State : &quot;, &quot;Density : &quot;, &quot;Electronegativity : &quot;]
24
25    scroll = tk.Scrollbar(root, orient=&quot;horizontal&quot;)
26    scroll.place(relx=0.05, rely=0.44, width=1000)
27
28    grand_canvas = tk.Canvas(root, bg='black', xscrollcommand=scroll.set)
29    grand_canvas.pack(fill=&quot;x&quot;, padx=20, pady=(0,40), side=tk.BOTTOM)
30
31
32    for i in elementals:
33        if search.get() in i or search.get() in str(i):
34            canvas1 = tk.Canvas(grand_canvas, bg='black', width=200, height=200)
35            canvas1.place(relx=0.02+results*0.18, rely=0.1)
36            results += 1
37            for x in range(10):
38                tk.Label(canvas1, text=f&quot;{texts[x]}{i[x]}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.08+var1*0.08)
39                var1 += 1
40            var1 = 0
41
42    scroll.config(command=grand_canvas.xview)
43        
44    tk.Label(root, text=f&quot;Number of resuts : {results}&quot;, bg='black', fg='white').place(relx=0.7, rely=0.3)
45
46tk.Button(root, text='Search', command=search_engine).place(relx=0.5, rely=0.3)
47
48root.mainloop()
49
50

And my Periodic Table Project , from where I extract data [Please name the file of this project as PT.py , then the code given above will work] .

1import tkinter as tk
2import PT
3
4root = tk.Tk()
5root.attributes('-fullscreen', True)
6root.config(bg='black')
7
8tk.Button(root, text='EXIT', command=root.destroy).place(x=0, y=0)
9
10elementals = PT.the_element_list
11
12search = tk.StringVar()
13
14search_entry = tk.Entry(root, textvariable=search)
15search_entry.place(relx=0.3, rely=0.3)
16
17
18def search_engine():
19
20    results = 0
21    var1 = 0
22
23    texts = [&quot;Name : &quot;, &quot;Atomic No. : &quot;, &quot;Atomic Mass : &quot;, &quot;Block : &quot;, &quot;Group No. : &quot;, &quot;Period No. : &quot;, &quot;Type : &quot;, &quot;State : &quot;, &quot;Density : &quot;, &quot;Electronegativity : &quot;]
24
25    scroll = tk.Scrollbar(root, orient=&quot;horizontal&quot;)
26    scroll.place(relx=0.05, rely=0.44, width=1000)
27
28    grand_canvas = tk.Canvas(root, bg='black', xscrollcommand=scroll.set)
29    grand_canvas.pack(fill=&quot;x&quot;, padx=20, pady=(0,40), side=tk.BOTTOM)
30
31
32    for i in elementals:
33        if search.get() in i or search.get() in str(i):
34            canvas1 = tk.Canvas(grand_canvas, bg='black', width=200, height=200)
35            canvas1.place(relx=0.02+results*0.18, rely=0.1)
36            results += 1
37            for x in range(10):
38                tk.Label(canvas1, text=f&quot;{texts[x]}{i[x]}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.08+var1*0.08)
39                var1 += 1
40            var1 = 0
41
42    scroll.config(command=grand_canvas.xview)
43        
44    tk.Label(root, text=f&quot;Number of resuts : {results}&quot;, bg='black', fg='white').place(relx=0.7, rely=0.3)
45
46tk.Button(root, text='Search', command=search_engine).place(relx=0.5, rely=0.3)
47
48root.mainloop()
49
50import tkinter as tk
51from functools import partial
52
53
54the_element_list = [['Hydrogen',1,'Non Metal',1,1,'s',1.01,'Gaseous',0.08,2.2],#H
55                    ['Helium',2,'Noble Gas',18,1,'s',4.00,'Gaseous',0.18,'Unavailable'],#He
56
57                    ['Lithium',3,'Alkaline Metal',1,2,'s',6.94,'Solid',0.53,0.98],#Li
58                    ['Beryllium',4,'Alkaline Earth Metal',2,2,'s',9.01,'Solid',1.84,1.57],#Be
59                    ['Boron',5,'Metalloid',13,2,'p',10.81,'Solid',2.46,2.04],#B
60                    ['Carbon',6,'Non Metal',14,2,'p',12.01,'Solid',2.26,2.55],#C
61                    ['Nitrogen',7,'Non Metal',15,2,'p',14.00,'Gaseous',1.17,3.04],#N
62                    ['Oxygen',8,'Non Metal',16,2,'p',15.99,'Gaseous',1.43,3.44],#O
63                    ['Fluorine',9,'Halogen',17,2,'p',18.99,'Gaseous',1.70,3.98],#F
64                    ['Neon',10,'Noble Gas',18,2,'p',20.17,'Gaseous',0.90,'Unavailable'],#Ne
65                
66                    ['Sodium',11,'Alkaline Metal',1,3,'s',22.99,'Solid',0.97,0.93],#Na
67                    ['Magnesium',12,'Alkaline Earth Metal',2,3,'s',24.31,'Solid',1.74,1.31],#Mg
68                    ['Aluminium',13,'Metal',13,3,'p',26.98,'Solid',2.69,1.61],#Al
69                    ['Silicon',14,'Metalloid',14,3,'p',28.08,'Solid',2.34,1.90],#Si
70                    ['Phosphorus',15,'Non Metal',15,3,'p',30.97,'Solid',2.4,2.19],#P
71                    ['Sulphur',16,'Non Metal',16,3,'p',32.06,'Solid',2.07,2.58],#S
72                    ['Chlorine',17,'Halogen',17,3,'p',35.45,'Gaseous',3.22,3.16],#Cl
73                    ['Argon',18,'Noble Gas',18,3,'p',39.95,'Gaseous',1.78,'Unavailable'],#Ar
74
75                    ['Potassium',19,'Alkaline Metal',1,4,'s',39.09,'Solid',0.86,0.82],#K
76                    ['Calicium',20,'Alkaline Earth Metal',2,4,'s',40.08,'Solid',1.55,1.00],#Ca                   
77                    ['Scandium',21,'Transition Metal',3,4,'d',44.96,'Solid',2.99,1.36],#Sc
78                    ['Titanium',22,'Transition Metal',4,4,'d',47.87,'Solid',4.5,1.54],#Ti
79                    ['Vanadium',23,'Transition Metal',5,4,'d',50.94,'Solid',6.11,1.63],#V
80                    ['Chromium',24,'Transition Metal',6,4,'d',51.99,'Solid',7.14,1.66],#Cr
81                    ['Manganese',25,'Transition Metal',7,4,'d',54.94,'Solid',7.43,1.55],#Mn
82                    ['Iron',26,'Transition Metal',8,4,'d',55.85,'Solid',7.87,1.83],#Fe
83                    ['Cobalt',27,'Transition Metal',9,4,'d',58.93,'Solid',8.90,1.88],#Co
84                    ['Nickel',28,'Transition Metal',10,4,'d',58.69,'Solid',8.90,1.91],#Ni                
85                    ['Copper',29,'Transition Metal',11,4,'d',63.54,'Solid',8.92,1.90],#Cu
86                    ['Zinc',30,'Transition Metal',12,4,'d',65.38,'Solid',7.14,1.65],#Zn
87                    ['Gallium',31,'Metal',13,4,'p',69.72,'Solid',5.90,1.81],#Ga
88                    ['Germanium',32,'Metalloid',14,4,'p',72.63,'Solid',5.32,2.01],#Ge
89                    ['Arsenic',33,'Metalloid',15,4,'p',74.92,'Solid',5.73,2.18],#As
90                    ['Selenium',34,'Metalloid',16,4,'p',78.97,'Solid',4.82,2.55],#Se
91                    ['Bromine',35,'Halogen',17,4,'p',79.90,'Liquid',3.12,2.96],#Br
92                    ['Krypton',36,'Noble Gas',18,4,'p',83.80,'Gaseous',3.75,3.00],#Kr
93                    
94                    ['Rubidium',37,'Alkaline Metal',1,5,'s',85.47,'Solid',1.53,0.82],#Rb
95                    ['Strontium',38,'Alkaline Earth Metal',2,5,'s',87.62,'Solid',2.63,0.95],#Sr
96                    ['Yttrium',39,'Transition Metal',3,5,'d',88.91,'Solid',4.47,1.22],#Y
97                    ['Zirconium',40,'Transition Metal',4,5,'d',91.22,'Solid',6.50,1.33],#Zr
98                    ['Niobium',41,'Transition Metal',5,5,'d',92.90,'Solid',8.57,1.6],#Nb
99                    ['Molybednium',42,'Transition Metal',6,5,'d',95.95,'Solid',10.28,2.16],#Mo
100                    ['Technetium',43,'Transition Metal',7,5,'d',98.90,'Solid',11.5,1.9],#Tc
101                    ['Ruthenium',44,'Transition Metal',8,5,'d',101.07,'Solid',12.37,2.2],#Ru                    
102                    ['Rhodium',45,'Transition Metal',9,5,'d',102.90,'Solid',12.38,2.28],#Rh
103                    ['Palladium',46,'Transition Metal',10,5,'d',106.42,'Solid',11.99,2.20],#Pd
104                    ['ilver',47,'Transition Metal',11,5,'d',107.87,'Solid',10.49,1.93],#Ag
105                    ['Cadmium',48,'Transition Metal',12,5,'d',112.41,'Solid',8.65,1.69],#Cd
106                    ['Indium',49,'Metal',13,5,'p',114.82,'Solid',7.31,1.78],#In
107                    ['Tin',50,'Metal',14,5,'p',118.71,'Solid',5.77,1.96],#Sn
108                    ['Antimony',51,'Metalloid',15,5,'p',121.76,'Solid',6.70,2.05],#Sb
109                    ['Tellurium',52,'Metalloid',16,5,'p',127.60,'Solid',6.24,2.10],#Te                
110                    ['Iodine',53,'Halogen',17,5,'p',126.90,'Solid',4.94,2.66],#I
111                    ['Xenon',54,'Noble Gas',18,5,'p',131.29,'Gaseous',5.90,2.6],#Xe
112
113                    ['Caesium',55,'Alkaline Metal',1,6,'s',132.91,'Solid',1.90,0.79],#Cs
114                    ['Barium',56,'Alkaline Earth Metal',2,6,'s',137.33,'Solid',3.62,0.89],#Ba
115
116                    ['Lanthanum',57,'Transition Metal',3,6,'d',138.90,'Solid',6.17,1.1],#La
117                    ['Cerium',58,'Lanthanide','La',6,'f',140.12,'Solid',6.77,1.12],#Ce
118                    ['Praseodymium',59,'Lanthanide','La',6,'f',140.91,'Solid',6.48,1.13],#Pr
119                    ['Neodymium',60,'Lanthanide','La',6,'f',144.24,'Solid',7.00,1.14],#Nd
120                    ['Promethium',61,'Lanthanide','La',6,'f',146.91,'Solid',7.2,'Unavailable.'],#Pm               
121                    ['Samarium',62,'Lanthanide','La',6,'f',150.36,'Solid',7.54,1.17],#Sm
122                    ['Europium',63,'Lanthanide','La',6,'f',151.96,'Solid',5.25,'Unavailable'],#Eu
123                    ['Gadolinium',64,'Lanthanide','La',6,'f',157.25,'Solid',7.89,1.20],#Gd
124                    ['Terbium',65,'Lanthanide','La',6,'f',158.93,'Solid',8.25,'Unavailable'],#Tb
125                    ['Dysprosium',66,'Lanthanide','La',6,'f',162.50,'Solid',8.56,1.22],#Dy
126                    ['Holmium',67,'Lanthanide','La',6,'f',164.93,'Solid',8.78,1.23],#Ho
127                    ['Erbium',68,'Lanthanide','La',6,'f',167.26,'Solid',9.05,1.24],#Er
128                    ['Thulium',69,'Lanthanide','La',6,'f',168.93,'Solid',9.32,1.25],#Tm                    
129                    ['Ytterbium',70,'Lanthanide','La',6,'f',173.05,'Solid',6.97,'Unavailable'],#Yb
130                    ['Lutetium',71,'Lanthanide','La',6,'f',174.97,'Solid',9.84,1.27],#Lu
131
132                    ['Hafnium',72,'Transition Metal',4,6,'d',178.49,'Solid',13.28,1.3],#Hf
133                    ['Tantalum',73,'Transition Metal',5,6,'d',180.95,'Solid',16.65,1.5],#Ta
134                    ['Tungsten',74,'Transition Metal',6,6,'d',183.84,'Solid',19.25,2.36],#W
135                    ['Rhenium',75,'Transition Metal',7,6,'d',186.21,'Solid',21.00,1.9],#Re
136                    ['Osmium',76,'Transition Metal',8,6,'d',190.23,'Solid',22.59,2.2],#Os
137                    ['Irdium',77,'Transition Metal',9,6,'d',192.22,'Solid',22.56,2.2],#Ir
138                    ['Platinum',78,'Transition Metal',10,6,'d',195.08,'Solid',21.45,2.2],#Pt
139                    ['Gold',79,'Transition Metal',11,6,'d',196.97,'Solid',19.32,2.54],#Au
140                    ['Mercury',80,'Transition Metal',12,6,'d',200.59,'Liquid',13.55,2.00],#Hg
141                    ['Thalium',81,'Metal',13,6,'p',204.38,'Solid',11.85,1.62],#Tl
142                    ['Lead',82,'Metal',14,6,'p',207.20,'Solid',11.34,2.33],#Pb
143                    ['Bismuth',83,'Metal',15,6,'p',208.98,'Solid',9.78,2.02],#Bi
144                    ['Polonium',84,'Metal',16,6,'p',209.98,'Solid',9.20,2.0],#Po
145                    ['Astatine',85,'Halogen',17,6,'p',209.99,'Solid','Unavailable',2.2],#At
146                    ['Radon',86,'Noble Gas',18,6,'p',222.00,'Gaseous',9.73,'Unavailable'],#Rn
147
148                    ['Francium',87,'Alkaline Metal',1,7,'s',223.02,'Solid','Unavailable',0.7],#Fr
149                    ['Radium',88,'Alkaline Earth Metal',2,7,'s',226.03,'Solid',5.5,0.9],#Ra
150
151                    ['Actinium',89,'Transition Metal',3,7,'d',227.03,'Solid',10.07,1.1],#Ac
152                    ['Thorium',90,'Actinide','Ac',7,'f',232.04,'Solid',11.72,1.3],#Th
153                    ['Protactinium',91,'Actinide','Ac',7,'f',231.04,'Solid',15.37,1.5],#Pa
154                    ['Uranium',92,'Actinide','Ac',7,'f',238.03,'Solid',19.16,1.38],#U
155                    ['Neptunium',93,'Actinide','Ac',7,'f',237.05,'Solid',20.45,1.36],#Np
156                    ['Plutonium',94,'Actinide','Ac',7,'f',244.06,'Solid',19.82,1.28],#Pu
157                    ['Americium',95,'Actinide','Ac',7,'f',243.06,'Solid',13.67,1.3],#Am                    
158                    ['Curium',96,'Actinide','Ac',7,'f',247.07,'Solid',13.51,1.3],#Cm
159                    ['Berkelium',97,'Actinide','Ac',7,'f',247,'Solid',14.78,1.3],#Bk
160                    ['Californium',98,'Actinide','Ac',7,'f',251,'Solid',15.1,1.3],#Cf
161                    ['Einsteinium',99,'Actinide','Ac',7,'f',252,'Solid',8.84,'Unavailable'],#Es
162                    ['Fermium',100,'Actinide','Ac',7,'f',257.10,'Solid','Unavailable','Unavailable'],#Fm
163                    ['Medelevium',101,'Actinide','Ac',7,'f',258,'Solid','Unavailable','Unavailable'],#Md
164                    ['Nobelium',102,'Actinide','Ac',7,'f',259,'Solid','Unavailable.','Unavailable'],#No
165                    ['Lawrencium',103,'Actinide','Ac',7,'f',266,'Solid','Unavailable','Unavailable'],#Lr
166                    
167                    ['Rutherfordium',104,'Transition Metal',4,7,'d',261.11,'Solid',17.00,'Unavailable'],#Rf
168                    ['Dubnium',105,'Transition Metal',5,7,'d',262.11,'Unavailable','Unavailable','Unavailable'],#Db
169                    ['Seaborgium',106,'Transition Metal',6,7,'d',263.12,'Unavailable','Unavailable','Unavailable'],#Sg
170                    ['Bohrium',107,'Transition Metal',7,7,'d',262.12,'Unavailable','Unavailable','Unavailable'],#Bh
171                    ['Hassium',108,'Transition Metal',8,7,'d',265,'Unavailable','Unavailable','Unavailable'],#Hs
172                    ['Meitnerium',109,'Unknown',9,7,'d',268,'Unavailable','Unavailable','Unavailable'],#Mt
173                    ['Darmstadtium',110,'Unknown',10,7,'d',281,'Unavailable','Unavailable','Unavailable'],#Ds
174                    ['Roentgenium',111,'Unknown',11,7,'d',280,'Unavailable','Unavailable','Unavailable'],#Rg
175                    ['Copernicium',112,'Unknown',12,7,'d',277,'Unavailable','Unavailable','Unavailable'],#Cn
176                    ['Nihonium',113,'Unknown',13,7,'p',287,'Unavailable','Unavailable','Unavailable'],#Nh
177                    ['Flerovium',114,'Unknown',14,7,'p',289,'Unavailable','Unavailable','Unavailable'],#Fl
178                    ['Moscovium',115,'Unknown',15,7,'p',288,'Unavailable','Unavailable','Unavailable'],#Mc
179                    ['Livermorium',116,'Unknown',16,7,'p',293,'Unavailable','Unavailable','Unavailable'],#Lv
180                    ['Tennessine',117,'Unknown',17,7,'p',292,'Unavailable','Unavailable','Unavailable'],#Ts
181                    ['Oganesson',118,'Unknown',18,7,'p',294,'Solid',6.6,'Unavailable']]#Og
182
183
184if __name__ == '__main__': # Listing the information of elements 
185
186    group_no_of_elements = [1] ; period_no_of_elements = [1] ; atomic_mass_of_elements = [1.01] ; block_of_elements = ['s'] ; name_of_elements = ['Hydrogen'] ; atomic_number_of_elements = [1] ; type_of_elements = ['Non Metal'] ; state_of_elements = ['Gaseous'] ; density_of_elements = [0.08] ; electronegativity_of_elements = [2.2]
187
188    group_no_of_elements_sec = [] ; period_no_of_elements_sec = [] ; atomic_mass_of_elements_sec = [] ; block_of_elements_sec = [] ; name_of_elements_sec = [] ; atomic_number_of_elements_sec = [] ; type_of_elements_sec = [] ; state_of_elements_sec = [] ; density_of_elements_sec = [] ; electronegativity_of_elements_sec = []
189
190
191    def add_to_list(a,b,c):
192
193        for i in range(a,b):
194            if c == ' ':
195                atomic_number_of_elements.append(&quot; &quot;)
196                name_of_elements.append(&quot; &quot;)
197                atomic_mass_of_elements.append(&quot; &quot;)
198                block_of_elements.append(&quot; &quot;)
199                group_no_of_elements.append(&quot; &quot;)
200                period_no_of_elements.append(&quot; &quot;)
201                type_of_elements.append(&quot; &quot;)
202                state_of_elements.append(&quot; &quot;)
203                density_of_elements.append(&quot; &quot;)
204                electronegativity_of_elements.append(&quot; &quot;)
205
206            elif c == 1:
207                name_of_elements.append(the_element_list[i][0])
208                atomic_number_of_elements.append(the_element_list[i][1])
209                type_of_elements.append(the_element_list[i][2])
210                group_no_of_elements.append(the_element_list[i][3])
211                period_no_of_elements.append(the_element_list[i][4])
212                block_of_elements.append(the_element_list[i][5])
213                atomic_mass_of_elements.append(the_element_list[i][6])
214                state_of_elements.append(the_element_list[i][7])
215                density_of_elements.append(the_element_list[i][8])
216                electronegativity_of_elements.append(the_element_list[i][9])
217
218            elif c == 2:
219                name_of_elements_sec.append(the_element_list[i][0])
220                atomic_number_of_elements_sec.append(the_element_list[i][1])
221                type_of_elements_sec.append(the_element_list[i][2])
222                group_no_of_elements_sec.append(the_element_list[i][3])
223                period_no_of_elements_sec.append(the_element_list[i][4])
224                block_of_elements_sec.append(the_element_list[i][5])
225                atomic_mass_of_elements_sec.append(the_element_list[i][6])
226                state_of_elements_sec.append(the_element_list[i][7])
227                density_of_elements_sec.append(the_element_list[i][8])
228                electronegativity_of_elements_sec.append(the_element_list[i][9])
229
230
231    add_to_list(0,16,' ')
232    add_to_list(1,4,1)
233    add_to_list(0,10,' ')
234    add_to_list(4,12,1)
235    add_to_list(0,10,' ')
236    add_to_list(12,56,1)
237    add_to_list(0,1,' ')
238    add_to_list(71,88,1)
239    add_to_list(0,1,' ')
240    add_to_list(103,118,1)
241    add_to_list(56,71,2)
242    add_to_list(88,103,2)
243
244
245if __name__ == '__main__': # Variables -- Toplevel Window's Names 
246
247    tk_window = []
248    for i in range(0,126):
249        tk_window.append(i)
250
251    tk_window_2 = []
252    for i in range(0,30):
253        tk_window_2.append(i)
254
255
256if __name__ == '__main__': # Variables -- Symbols of elements in the Periodic Table 
257
258    period_1 = ['H' ,'','','','','','','','','','','','','','','','','He']
259    period_2 = ['Li','Be','','','','','','','','','','','B','C','N','O','F','Ne']
260    period_3 = ['Na','Mg','','','','','','','','','','','Al','Si','P','S','Cl','Ar']
261    period_4 = &quot;&quot;&quot;K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr&quot;&quot;&quot;.split(&quot; &quot;)
262    period_5 = &quot;&quot;&quot;Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe&quot;&quot;&quot;.split(&quot; &quot;)
263    period_6 = &quot;&quot;&quot;Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn&quot;&quot;&quot;.split(&quot; &quot;)
264    period_7 = &quot;&quot;&quot;Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og&quot;&quot;&quot;.split(&quot; &quot;)
265
266    period_6a = &quot;&quot;&quot;La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu&quot;&quot;&quot;.split(&quot; &quot;)
267    period_7a = &quot;&quot;&quot;Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr&quot;&quot;&quot;.split(&quot; &quot;)
268
269    # Making a list of main elements and secondary elements
270    main = period_1 + period_2 + period_3 + period_4 + period_5 + period_6 + period_7
271    sec  = period_6a + period_7a
272
273
274if __name__ == '__main__': # Variables -- Colours for each group of simliar elements in the Periodic Table 
275
276    # Colors for each group
277    non_m_col   = '#feab90'
278    alk_m_col   = '#ffe0b2'
279    alk_ea_col  = '#fecc81'
280    trans_m_col = '#d2c4e8'
281    halogen_col = '#a4d7a7'
282    metals_col  = '#feab90'
283    noble_g_col = '#fefffe'
284    act_col     = '#b2e5fd'
285    rare_m_col  = '#e7ee9a'
286    plain_but_col = 'grey' 
287
288
289if __name__ == '__main__': # Using tkinter to display a Periodic Table of Elements
290
291    root = tk.Tk()
292    root.attributes('-fullscreen', True)
293    root.config(bg='purple')
294
295    tk.Label(root, text=&quot;Periodic Table of Elements&quot;, bg='purple', fg='white', font=['Bookman Old Style', 40, 'bold', 'underline']).place(relx=0.15, rely=0.02)
296
297
298    if __name__ == '__main__': # Frames for the Periodic Table
299
300        # Frame for the entire table
301        period_tab = tk.Frame(root, bg='grey', highlightbackground='black', highlightthickness=20)
302        period_tab.pack(side=tk.BOTTOM, pady=(0,50))
303
304        # Frame for the main elements only
305        main_elem = tk.Frame(period_tab)
306        main_elem.pack(padx=20, pady=20)
307
308        # Frame for the secondary elements only
309        sec_elem = tk.Frame(period_tab)
310        sec_elem.pack(pady=20, padx=20)
311
312
313    if __name__ == '__main__': # Function which creates a window of info for each element
314
315        def PT(window, name, symbol, atom_no, atom_mass, block, group, period, type, state, density, electronegativity):
316            if atom_no == ' ':
317                pass
318            else:
319                window = tk.Toplevel(root)
320                window.geometry('400x400')
321                window.config(bg='black')
322
323                tk.Label(window, text=name, bg='black', fg='white').place(relx=0.3, rely=0.1)
324
325                tk.Label(window, text=f&quot;Symbol : {symbol}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.3)
326                tk.Label(window, text=f&quot;Atomic Number : {atom_no}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.35)
327                tk.Label(window, text=f&quot;Atomic Mass : {atom_mass}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.4)
328                tk.Label(window, text=f&quot;Block : {block}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.45)
329                tk.Label(window, text=f&quot;Group Number : {group}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.5)
330                tk.Label(window, text=f&quot;Period Number : {period}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.55)
331                tk.Label(window, text=f&quot;Type of element : {type}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.6)
332                tk.Label(window, text=f&quot;State of element : {state}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.65)
333                tk.Label(window, text=f&quot;Density : {density}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.7)
334                tk.Label(window, text=f&quot;Electronegativity : {electronegativity}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.75)
335
336
337    if __name__ == '__main__': # Creating buttons for each element in the Periodic Table
338
339        # Creating a 7x18 table of buttons and appending it to a 2D python list for main elements
340        buttons = []
341        for i in range(7):
342            temp = []
343            for j in range(18):
344                but = tk.Button(main_elem,text=main[18*i+j],width=8,bg='#f0f0f0', command=partial(PT, tk_window[18*i+j], name_of_elements[18*i+j], main[18*i+j], atomic_number_of_elements[18*i+j], atomic_mass_of_elements[18*i+j], block_of_elements[18*i+j], group_no_of_elements[18*i+j], period_no_of_elements[18*i+j], type_of_elements[18*i+j], state_of_elements[18*i+j], density_of_elements[18*i+j], electronegativity_of_elements[18*i+j]))
345                but.grid(row=i,column=j)
346                temp.append(but)
347            buttons.append(temp)
348
349        # Creating a 2x15 table of buttons for secondary elements
350        for i in range(2):
351            for j in range(15):
352                if i == 0: # If row 1 then different color
353                    tk.Button(sec_elem,text=sec[15*i+j],width=8,bg=rare_m_col, command=partial(PT, tk_window_2[15*i+j], name_of_elements_sec[15*i+j], sec[15*i+j], atomic_number_of_elements_sec[15*i+j], atomic_mass_of_elements_sec[15*i+j], block_of_elements_sec[15*i+j], group_no_of_elements_sec[15*i+j], period_no_of_elements_sec[15*i+j], type_of_elements_sec[15*i+j], state_of_elements_sec[15*i+j], density_of_elements_sec[15*i+j], electronegativity_of_elements_sec[15*i+j])).grid(row=i,column=j)
354                else:
355                    tk.Button(sec_elem,text=sec[15*i+j],width=8,bg=act_col, command=partial(PT, tk_window_2[15*i+j], name_of_elements_sec[15*i+j], sec[15*i+j], atomic_number_of_elements_sec[15*i+j], atomic_mass_of_elements_sec[15*i+j], block_of_elements_sec[15*i+j], group_no_of_elements_sec[15*i+j], period_no_of_elements_sec[15*i+j], type_of_elements_sec[15*i+j], state_of_elements_sec[15*i+j], density_of_elements_sec[15*i+j], electronegativity_of_elements_sec[15*i+j])).grid(row=i,column=j)
356
357
358    if __name__ == '__main__': # Setting colours for each button
359
360        # Manually pick out main elements from the table
361        non_metals     = buttons[0][0],buttons[1][12:16],buttons[2][13:16],buttons[3][14:16],buttons[4][15]
362        alk_metals     = [row[0] for row in buttons[1:]]
363        alk_ea_metals  = [row[1] for row in buttons[1:]]
364        halogens       = [row[16] for row in buttons[1:]]
365        noble_gases    = [row[-1] for row in buttons[:]]
366        transition_met = [buttons[x][3:12] for x in range(3,7)]
367        metals         = buttons[6][12:16],buttons[5][12:16],buttons[4][12:15],buttons[3][12:14],buttons[2][12]
368        rare_metals    = [row[2] for row in buttons[3:6]]
369        actinoid       = buttons[-1][2]
370        plain_but      = buttons[0][1:-1],buttons[1][2:12],buttons[2][2:12]
371
372        # Change colors for those main element buttons
373        actinoid['bg'] = act_col
374        for i in alk_metals:
375            i['bg'] = alk_m_col
376        for i in alk_ea_metals: 
377            i['bg'] = alk_ea_col
378        for i in halogens:      
379            i['bg'] = halogen_col
380        for i in noble_gases:   
381            i['bg'] = noble_g_col
382        for i in rare_metals:   
383            i['bg'] = rare_m_col
384
385        for i in transition_met:
386            for j in i:
387                j['bg'] = trans_m_col
388
389        for i in plain_but:
390            for j in i:
391                j['bg'] = plain_but_col
392
393        for i in non_metals:
394            if isinstance(i,list):
395                for j in i:
396                    j.config(bg=non_m_col)
397            else:
398                i.config(bg=non_m_col)
399
400        for i in metals:
401            if isinstance(i,list):
402                for j in i:
403                    j.config(bg=metals_col)
404            else:
405                i.config(bg=metals_col)
406        
407        for i in plain_but:
408            for j in i:
409                j['relief'] = 'flat'
410
411
412    if __name__ == '__main__': # Creating a frame for categorizing elements on basis of their colours 
413        legends_frame = tk.Canvas(root, bg='black', width=200, height=250)
414
415        tk.Label(legends_frame, text='Categories', bg='black', fg='white', font=['Bookman Old Style', 10, 'underline']).place(relx=0.25, rely=0.03)
416
417        labels = ['Actinides', 'Alkali Metals', 'Alkali Earth Metals', 'Halogens', 'Lanthanides', 'Metalloids', 'Noble Gases',  'Non Metals', 'Transition Metals']
418        colours = [act_col, alk_m_col, alk_ea_col, halogen_col, rare_m_col, metals_col, noble_g_col, non_m_col, trans_m_col]
419
420        for i in range(0,9):
421            tk.Label(legends_frame, text=labels[i], bg='black', fg=colours[i]).place(relx=0.4, rely=0.16+i*0.09)
422            tk.Canvas(legends_frame, bg=colours[i], width=32, height=12, borderwidth=0).place(relx=0.1, rely=0.16+i*0.09)
423
424        legends_frame.place(relx=0.8, rely=0.05)
425
426
427    tk.Button(root,text='EXIT',command=root.destroy).place(x=10, y=10)
428
429    root.mainloop()
430

Further Notes : If anyone could suggest me some tips through which I can write better codes , I would be grateful to him/her .

ANSWER

Answered 2021-Dec-29 at 20:33

I rewrote your code with some better ways to create table. My idea was to pick out the buttons that fell onto a range of type and then loop through those buttons and change its color to those type.

1import tkinter as tk
2import PT
3
4root = tk.Tk()
5root.attributes('-fullscreen', True)
6root.config(bg='black')
7
8tk.Button(root, text='EXIT', command=root.destroy).place(x=0, y=0)
9
10elementals = PT.the_element_list
11
12search = tk.StringVar()
13
14search_entry = tk.Entry(root, textvariable=search)
15search_entry.place(relx=0.3, rely=0.3)
16
17
18def search_engine():
19
20    results = 0
21    var1 = 0
22
23    texts = [&quot;Name : &quot;, &quot;Atomic No. : &quot;, &quot;Atomic Mass : &quot;, &quot;Block : &quot;, &quot;Group No. : &quot;, &quot;Period No. : &quot;, &quot;Type : &quot;, &quot;State : &quot;, &quot;Density : &quot;, &quot;Electronegativity : &quot;]
24
25    scroll = tk.Scrollbar(root, orient=&quot;horizontal&quot;)
26    scroll.place(relx=0.05, rely=0.44, width=1000)
27
28    grand_canvas = tk.Canvas(root, bg='black', xscrollcommand=scroll.set)
29    grand_canvas.pack(fill=&quot;x&quot;, padx=20, pady=(0,40), side=tk.BOTTOM)
30
31
32    for i in elementals:
33        if search.get() in i or search.get() in str(i):
34            canvas1 = tk.Canvas(grand_canvas, bg='black', width=200, height=200)
35            canvas1.place(relx=0.02+results*0.18, rely=0.1)
36            results += 1
37            for x in range(10):
38                tk.Label(canvas1, text=f&quot;{texts[x]}{i[x]}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.08+var1*0.08)
39                var1 += 1
40            var1 = 0
41
42    scroll.config(command=grand_canvas.xview)
43        
44    tk.Label(root, text=f&quot;Number of resuts : {results}&quot;, bg='black', fg='white').place(relx=0.7, rely=0.3)
45
46tk.Button(root, text='Search', command=search_engine).place(relx=0.5, rely=0.3)
47
48root.mainloop()
49
50import tkinter as tk
51from functools import partial
52
53
54the_element_list = [['Hydrogen',1,'Non Metal',1,1,'s',1.01,'Gaseous',0.08,2.2],#H
55                    ['Helium',2,'Noble Gas',18,1,'s',4.00,'Gaseous',0.18,'Unavailable'],#He
56
57                    ['Lithium',3,'Alkaline Metal',1,2,'s',6.94,'Solid',0.53,0.98],#Li
58                    ['Beryllium',4,'Alkaline Earth Metal',2,2,'s',9.01,'Solid',1.84,1.57],#Be
59                    ['Boron',5,'Metalloid',13,2,'p',10.81,'Solid',2.46,2.04],#B
60                    ['Carbon',6,'Non Metal',14,2,'p',12.01,'Solid',2.26,2.55],#C
61                    ['Nitrogen',7,'Non Metal',15,2,'p',14.00,'Gaseous',1.17,3.04],#N
62                    ['Oxygen',8,'Non Metal',16,2,'p',15.99,'Gaseous',1.43,3.44],#O
63                    ['Fluorine',9,'Halogen',17,2,'p',18.99,'Gaseous',1.70,3.98],#F
64                    ['Neon',10,'Noble Gas',18,2,'p',20.17,'Gaseous',0.90,'Unavailable'],#Ne
65                
66                    ['Sodium',11,'Alkaline Metal',1,3,'s',22.99,'Solid',0.97,0.93],#Na
67                    ['Magnesium',12,'Alkaline Earth Metal',2,3,'s',24.31,'Solid',1.74,1.31],#Mg
68                    ['Aluminium',13,'Metal',13,3,'p',26.98,'Solid',2.69,1.61],#Al
69                    ['Silicon',14,'Metalloid',14,3,'p',28.08,'Solid',2.34,1.90],#Si
70                    ['Phosphorus',15,'Non Metal',15,3,'p',30.97,'Solid',2.4,2.19],#P
71                    ['Sulphur',16,'Non Metal',16,3,'p',32.06,'Solid',2.07,2.58],#S
72                    ['Chlorine',17,'Halogen',17,3,'p',35.45,'Gaseous',3.22,3.16],#Cl
73                    ['Argon',18,'Noble Gas',18,3,'p',39.95,'Gaseous',1.78,'Unavailable'],#Ar
74
75                    ['Potassium',19,'Alkaline Metal',1,4,'s',39.09,'Solid',0.86,0.82],#K
76                    ['Calicium',20,'Alkaline Earth Metal',2,4,'s',40.08,'Solid',1.55,1.00],#Ca                   
77                    ['Scandium',21,'Transition Metal',3,4,'d',44.96,'Solid',2.99,1.36],#Sc
78                    ['Titanium',22,'Transition Metal',4,4,'d',47.87,'Solid',4.5,1.54],#Ti
79                    ['Vanadium',23,'Transition Metal',5,4,'d',50.94,'Solid',6.11,1.63],#V
80                    ['Chromium',24,'Transition Metal',6,4,'d',51.99,'Solid',7.14,1.66],#Cr
81                    ['Manganese',25,'Transition Metal',7,4,'d',54.94,'Solid',7.43,1.55],#Mn
82                    ['Iron',26,'Transition Metal',8,4,'d',55.85,'Solid',7.87,1.83],#Fe
83                    ['Cobalt',27,'Transition Metal',9,4,'d',58.93,'Solid',8.90,1.88],#Co
84                    ['Nickel',28,'Transition Metal',10,4,'d',58.69,'Solid',8.90,1.91],#Ni                
85                    ['Copper',29,'Transition Metal',11,4,'d',63.54,'Solid',8.92,1.90],#Cu
86                    ['Zinc',30,'Transition Metal',12,4,'d',65.38,'Solid',7.14,1.65],#Zn
87                    ['Gallium',31,'Metal',13,4,'p',69.72,'Solid',5.90,1.81],#Ga
88                    ['Germanium',32,'Metalloid',14,4,'p',72.63,'Solid',5.32,2.01],#Ge
89                    ['Arsenic',33,'Metalloid',15,4,'p',74.92,'Solid',5.73,2.18],#As
90                    ['Selenium',34,'Metalloid',16,4,'p',78.97,'Solid',4.82,2.55],#Se
91                    ['Bromine',35,'Halogen',17,4,'p',79.90,'Liquid',3.12,2.96],#Br
92                    ['Krypton',36,'Noble Gas',18,4,'p',83.80,'Gaseous',3.75,3.00],#Kr
93                    
94                    ['Rubidium',37,'Alkaline Metal',1,5,'s',85.47,'Solid',1.53,0.82],#Rb
95                    ['Strontium',38,'Alkaline Earth Metal',2,5,'s',87.62,'Solid',2.63,0.95],#Sr
96                    ['Yttrium',39,'Transition Metal',3,5,'d',88.91,'Solid',4.47,1.22],#Y
97                    ['Zirconium',40,'Transition Metal',4,5,'d',91.22,'Solid',6.50,1.33],#Zr
98                    ['Niobium',41,'Transition Metal',5,5,'d',92.90,'Solid',8.57,1.6],#Nb
99                    ['Molybednium',42,'Transition Metal',6,5,'d',95.95,'Solid',10.28,2.16],#Mo
100                    ['Technetium',43,'Transition Metal',7,5,'d',98.90,'Solid',11.5,1.9],#Tc
101                    ['Ruthenium',44,'Transition Metal',8,5,'d',101.07,'Solid',12.37,2.2],#Ru                    
102                    ['Rhodium',45,'Transition Metal',9,5,'d',102.90,'Solid',12.38,2.28],#Rh
103                    ['Palladium',46,'Transition Metal',10,5,'d',106.42,'Solid',11.99,2.20],#Pd
104                    ['ilver',47,'Transition Metal',11,5,'d',107.87,'Solid',10.49,1.93],#Ag
105                    ['Cadmium',48,'Transition Metal',12,5,'d',112.41,'Solid',8.65,1.69],#Cd
106                    ['Indium',49,'Metal',13,5,'p',114.82,'Solid',7.31,1.78],#In
107                    ['Tin',50,'Metal',14,5,'p',118.71,'Solid',5.77,1.96],#Sn
108                    ['Antimony',51,'Metalloid',15,5,'p',121.76,'Solid',6.70,2.05],#Sb
109                    ['Tellurium',52,'Metalloid',16,5,'p',127.60,'Solid',6.24,2.10],#Te                
110                    ['Iodine',53,'Halogen',17,5,'p',126.90,'Solid',4.94,2.66],#I
111                    ['Xenon',54,'Noble Gas',18,5,'p',131.29,'Gaseous',5.90,2.6],#Xe
112
113                    ['Caesium',55,'Alkaline Metal',1,6,'s',132.91,'Solid',1.90,0.79],#Cs
114                    ['Barium',56,'Alkaline Earth Metal',2,6,'s',137.33,'Solid',3.62,0.89],#Ba
115
116                    ['Lanthanum',57,'Transition Metal',3,6,'d',138.90,'Solid',6.17,1.1],#La
117                    ['Cerium',58,'Lanthanide','La',6,'f',140.12,'Solid',6.77,1.12],#Ce
118                    ['Praseodymium',59,'Lanthanide','La',6,'f',140.91,'Solid',6.48,1.13],#Pr
119                    ['Neodymium',60,'Lanthanide','La',6,'f',144.24,'Solid',7.00,1.14],#Nd
120                    ['Promethium',61,'Lanthanide','La',6,'f',146.91,'Solid',7.2,'Unavailable.'],#Pm               
121                    ['Samarium',62,'Lanthanide','La',6,'f',150.36,'Solid',7.54,1.17],#Sm
122                    ['Europium',63,'Lanthanide','La',6,'f',151.96,'Solid',5.25,'Unavailable'],#Eu
123                    ['Gadolinium',64,'Lanthanide','La',6,'f',157.25,'Solid',7.89,1.20],#Gd
124                    ['Terbium',65,'Lanthanide','La',6,'f',158.93,'Solid',8.25,'Unavailable'],#Tb
125                    ['Dysprosium',66,'Lanthanide','La',6,'f',162.50,'Solid',8.56,1.22],#Dy
126                    ['Holmium',67,'Lanthanide','La',6,'f',164.93,'Solid',8.78,1.23],#Ho
127                    ['Erbium',68,'Lanthanide','La',6,'f',167.26,'Solid',9.05,1.24],#Er
128                    ['Thulium',69,'Lanthanide','La',6,'f',168.93,'Solid',9.32,1.25],#Tm                    
129                    ['Ytterbium',70,'Lanthanide','La',6,'f',173.05,'Solid',6.97,'Unavailable'],#Yb
130                    ['Lutetium',71,'Lanthanide','La',6,'f',174.97,'Solid',9.84,1.27],#Lu
131
132                    ['Hafnium',72,'Transition Metal',4,6,'d',178.49,'Solid',13.28,1.3],#Hf
133                    ['Tantalum',73,'Transition Metal',5,6,'d',180.95,'Solid',16.65,1.5],#Ta
134                    ['Tungsten',74,'Transition Metal',6,6,'d',183.84,'Solid',19.25,2.36],#W
135                    ['Rhenium',75,'Transition Metal',7,6,'d',186.21,'Solid',21.00,1.9],#Re
136                    ['Osmium',76,'Transition Metal',8,6,'d',190.23,'Solid',22.59,2.2],#Os
137                    ['Irdium',77,'Transition Metal',9,6,'d',192.22,'Solid',22.56,2.2],#Ir
138                    ['Platinum',78,'Transition Metal',10,6,'d',195.08,'Solid',21.45,2.2],#Pt
139                    ['Gold',79,'Transition Metal',11,6,'d',196.97,'Solid',19.32,2.54],#Au
140                    ['Mercury',80,'Transition Metal',12,6,'d',200.59,'Liquid',13.55,2.00],#Hg
141                    ['Thalium',81,'Metal',13,6,'p',204.38,'Solid',11.85,1.62],#Tl
142                    ['Lead',82,'Metal',14,6,'p',207.20,'Solid',11.34,2.33],#Pb
143                    ['Bismuth',83,'Metal',15,6,'p',208.98,'Solid',9.78,2.02],#Bi
144                    ['Polonium',84,'Metal',16,6,'p',209.98,'Solid',9.20,2.0],#Po
145                    ['Astatine',85,'Halogen',17,6,'p',209.99,'Solid','Unavailable',2.2],#At
146                    ['Radon',86,'Noble Gas',18,6,'p',222.00,'Gaseous',9.73,'Unavailable'],#Rn
147
148                    ['Francium',87,'Alkaline Metal',1,7,'s',223.02,'Solid','Unavailable',0.7],#Fr
149                    ['Radium',88,'Alkaline Earth Metal',2,7,'s',226.03,'Solid',5.5,0.9],#Ra
150
151                    ['Actinium',89,'Transition Metal',3,7,'d',227.03,'Solid',10.07,1.1],#Ac
152                    ['Thorium',90,'Actinide','Ac',7,'f',232.04,'Solid',11.72,1.3],#Th
153                    ['Protactinium',91,'Actinide','Ac',7,'f',231.04,'Solid',15.37,1.5],#Pa
154                    ['Uranium',92,'Actinide','Ac',7,'f',238.03,'Solid',19.16,1.38],#U
155                    ['Neptunium',93,'Actinide','Ac',7,'f',237.05,'Solid',20.45,1.36],#Np
156                    ['Plutonium',94,'Actinide','Ac',7,'f',244.06,'Solid',19.82,1.28],#Pu
157                    ['Americium',95,'Actinide','Ac',7,'f',243.06,'Solid',13.67,1.3],#Am                    
158                    ['Curium',96,'Actinide','Ac',7,'f',247.07,'Solid',13.51,1.3],#Cm
159                    ['Berkelium',97,'Actinide','Ac',7,'f',247,'Solid',14.78,1.3],#Bk
160                    ['Californium',98,'Actinide','Ac',7,'f',251,'Solid',15.1,1.3],#Cf
161                    ['Einsteinium',99,'Actinide','Ac',7,'f',252,'Solid',8.84,'Unavailable'],#Es
162                    ['Fermium',100,'Actinide','Ac',7,'f',257.10,'Solid','Unavailable','Unavailable'],#Fm
163                    ['Medelevium',101,'Actinide','Ac',7,'f',258,'Solid','Unavailable','Unavailable'],#Md
164                    ['Nobelium',102,'Actinide','Ac',7,'f',259,'Solid','Unavailable.','Unavailable'],#No
165                    ['Lawrencium',103,'Actinide','Ac',7,'f',266,'Solid','Unavailable','Unavailable'],#Lr
166                    
167                    ['Rutherfordium',104,'Transition Metal',4,7,'d',261.11,'Solid',17.00,'Unavailable'],#Rf
168                    ['Dubnium',105,'Transition Metal',5,7,'d',262.11,'Unavailable','Unavailable','Unavailable'],#Db
169                    ['Seaborgium',106,'Transition Metal',6,7,'d',263.12,'Unavailable','Unavailable','Unavailable'],#Sg
170                    ['Bohrium',107,'Transition Metal',7,7,'d',262.12,'Unavailable','Unavailable','Unavailable'],#Bh
171                    ['Hassium',108,'Transition Metal',8,7,'d',265,'Unavailable','Unavailable','Unavailable'],#Hs
172                    ['Meitnerium',109,'Unknown',9,7,'d',268,'Unavailable','Unavailable','Unavailable'],#Mt
173                    ['Darmstadtium',110,'Unknown',10,7,'d',281,'Unavailable','Unavailable','Unavailable'],#Ds
174                    ['Roentgenium',111,'Unknown',11,7,'d',280,'Unavailable','Unavailable','Unavailable'],#Rg
175                    ['Copernicium',112,'Unknown',12,7,'d',277,'Unavailable','Unavailable','Unavailable'],#Cn
176                    ['Nihonium',113,'Unknown',13,7,'p',287,'Unavailable','Unavailable','Unavailable'],#Nh
177                    ['Flerovium',114,'Unknown',14,7,'p',289,'Unavailable','Unavailable','Unavailable'],#Fl
178                    ['Moscovium',115,'Unknown',15,7,'p',288,'Unavailable','Unavailable','Unavailable'],#Mc
179                    ['Livermorium',116,'Unknown',16,7,'p',293,'Unavailable','Unavailable','Unavailable'],#Lv
180                    ['Tennessine',117,'Unknown',17,7,'p',292,'Unavailable','Unavailable','Unavailable'],#Ts
181                    ['Oganesson',118,'Unknown',18,7,'p',294,'Solid',6.6,'Unavailable']]#Og
182
183
184if __name__ == '__main__': # Listing the information of elements 
185
186    group_no_of_elements = [1] ; period_no_of_elements = [1] ; atomic_mass_of_elements = [1.01] ; block_of_elements = ['s'] ; name_of_elements = ['Hydrogen'] ; atomic_number_of_elements = [1] ; type_of_elements = ['Non Metal'] ; state_of_elements = ['Gaseous'] ; density_of_elements = [0.08] ; electronegativity_of_elements = [2.2]
187
188    group_no_of_elements_sec = [] ; period_no_of_elements_sec = [] ; atomic_mass_of_elements_sec = [] ; block_of_elements_sec = [] ; name_of_elements_sec = [] ; atomic_number_of_elements_sec = [] ; type_of_elements_sec = [] ; state_of_elements_sec = [] ; density_of_elements_sec = [] ; electronegativity_of_elements_sec = []
189
190
191    def add_to_list(a,b,c):
192
193        for i in range(a,b):
194            if c == ' ':
195                atomic_number_of_elements.append(&quot; &quot;)
196                name_of_elements.append(&quot; &quot;)
197                atomic_mass_of_elements.append(&quot; &quot;)
198                block_of_elements.append(&quot; &quot;)
199                group_no_of_elements.append(&quot; &quot;)
200                period_no_of_elements.append(&quot; &quot;)
201                type_of_elements.append(&quot; &quot;)
202                state_of_elements.append(&quot; &quot;)
203                density_of_elements.append(&quot; &quot;)
204                electronegativity_of_elements.append(&quot; &quot;)
205
206            elif c == 1:
207                name_of_elements.append(the_element_list[i][0])
208                atomic_number_of_elements.append(the_element_list[i][1])
209                type_of_elements.append(the_element_list[i][2])
210                group_no_of_elements.append(the_element_list[i][3])
211                period_no_of_elements.append(the_element_list[i][4])
212                block_of_elements.append(the_element_list[i][5])
213                atomic_mass_of_elements.append(the_element_list[i][6])
214                state_of_elements.append(the_element_list[i][7])
215                density_of_elements.append(the_element_list[i][8])
216                electronegativity_of_elements.append(the_element_list[i][9])
217
218            elif c == 2:
219                name_of_elements_sec.append(the_element_list[i][0])
220                atomic_number_of_elements_sec.append(the_element_list[i][1])
221                type_of_elements_sec.append(the_element_list[i][2])
222                group_no_of_elements_sec.append(the_element_list[i][3])
223                period_no_of_elements_sec.append(the_element_list[i][4])
224                block_of_elements_sec.append(the_element_list[i][5])
225                atomic_mass_of_elements_sec.append(the_element_list[i][6])
226                state_of_elements_sec.append(the_element_list[i][7])
227                density_of_elements_sec.append(the_element_list[i][8])
228                electronegativity_of_elements_sec.append(the_element_list[i][9])
229
230
231    add_to_list(0,16,' ')
232    add_to_list(1,4,1)
233    add_to_list(0,10,' ')
234    add_to_list(4,12,1)
235    add_to_list(0,10,' ')
236    add_to_list(12,56,1)
237    add_to_list(0,1,' ')
238    add_to_list(71,88,1)
239    add_to_list(0,1,' ')
240    add_to_list(103,118,1)
241    add_to_list(56,71,2)
242    add_to_list(88,103,2)
243
244
245if __name__ == '__main__': # Variables -- Toplevel Window's Names 
246
247    tk_window = []
248    for i in range(0,126):
249        tk_window.append(i)
250
251    tk_window_2 = []
252    for i in range(0,30):
253        tk_window_2.append(i)
254
255
256if __name__ == '__main__': # Variables -- Symbols of elements in the Periodic Table 
257
258    period_1 = ['H' ,'','','','','','','','','','','','','','','','','He']
259    period_2 = ['Li','Be','','','','','','','','','','','B','C','N','O','F','Ne']
260    period_3 = ['Na','Mg','','','','','','','','','','','Al','Si','P','S','Cl','Ar']
261    period_4 = &quot;&quot;&quot;K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr&quot;&quot;&quot;.split(&quot; &quot;)
262    period_5 = &quot;&quot;&quot;Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe&quot;&quot;&quot;.split(&quot; &quot;)
263    period_6 = &quot;&quot;&quot;Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn&quot;&quot;&quot;.split(&quot; &quot;)
264    period_7 = &quot;&quot;&quot;Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og&quot;&quot;&quot;.split(&quot; &quot;)
265
266    period_6a = &quot;&quot;&quot;La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu&quot;&quot;&quot;.split(&quot; &quot;)
267    period_7a = &quot;&quot;&quot;Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr&quot;&quot;&quot;.split(&quot; &quot;)
268
269    # Making a list of main elements and secondary elements
270    main = period_1 + period_2 + period_3 + period_4 + period_5 + period_6 + period_7
271    sec  = period_6a + period_7a
272
273
274if __name__ == '__main__': # Variables -- Colours for each group of simliar elements in the Periodic Table 
275
276    # Colors for each group
277    non_m_col   = '#feab90'
278    alk_m_col   = '#ffe0b2'
279    alk_ea_col  = '#fecc81'
280    trans_m_col = '#d2c4e8'
281    halogen_col = '#a4d7a7'
282    metals_col  = '#feab90'
283    noble_g_col = '#fefffe'
284    act_col     = '#b2e5fd'
285    rare_m_col  = '#e7ee9a'
286    plain_but_col = 'grey' 
287
288
289if __name__ == '__main__': # Using tkinter to display a Periodic Table of Elements
290
291    root = tk.Tk()
292    root.attributes('-fullscreen', True)
293    root.config(bg='purple')
294
295    tk.Label(root, text=&quot;Periodic Table of Elements&quot;, bg='purple', fg='white', font=['Bookman Old Style', 40, 'bold', 'underline']).place(relx=0.15, rely=0.02)
296
297
298    if __name__ == '__main__': # Frames for the Periodic Table
299
300        # Frame for the entire table
301        period_tab = tk.Frame(root, bg='grey', highlightbackground='black', highlightthickness=20)
302        period_tab.pack(side=tk.BOTTOM, pady=(0,50))
303
304        # Frame for the main elements only
305        main_elem = tk.Frame(period_tab)
306        main_elem.pack(padx=20, pady=20)
307
308        # Frame for the secondary elements only
309        sec_elem = tk.Frame(period_tab)
310        sec_elem.pack(pady=20, padx=20)
311
312
313    if __name__ == '__main__': # Function which creates a window of info for each element
314
315        def PT(window, name, symbol, atom_no, atom_mass, block, group, period, type, state, density, electronegativity):
316            if atom_no == ' ':
317                pass
318            else:
319                window = tk.Toplevel(root)
320                window.geometry('400x400')
321                window.config(bg='black')
322
323                tk.Label(window, text=name, bg='black', fg='white').place(relx=0.3, rely=0.1)
324
325                tk.Label(window, text=f&quot;Symbol : {symbol}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.3)
326                tk.Label(window, text=f&quot;Atomic Number : {atom_no}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.35)
327                tk.Label(window, text=f&quot;Atomic Mass : {atom_mass}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.4)
328                tk.Label(window, text=f&quot;Block : {block}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.45)
329                tk.Label(window, text=f&quot;Group Number : {group}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.5)
330                tk.Label(window, text=f&quot;Period Number : {period}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.55)
331                tk.Label(window, text=f&quot;Type of element : {type}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.6)
332                tk.Label(window, text=f&quot;State of element : {state}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.65)
333                tk.Label(window, text=f&quot;Density : {density}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.7)
334                tk.Label(window, text=f&quot;Electronegativity : {electronegativity}&quot;, bg='black', fg='white').place(relx=0.05, rely=0.75)
335
336
337    if __name__ == '__main__': # Creating buttons for each element in the Periodic Table
338
339        # Creating a 7x18 table of buttons and appending it to a 2D python list for main elements
340        buttons = []
341        for i in range(7):
342            temp = []
343            for j in range(18):
344                but = tk.Button(main_elem,text=main[18*i+j],width=8,bg='#f0f0f0', command=partial(PT, tk_window[18*i+j], name_of_elements[18*i+j], main[18*i+j], atomic_number_of_elements[18*i+j], atomic_mass_of_elements[18*i+j], block_of_elements[18*i+j], group_no_of_elements[18*i+j], period_no_of_elements[18*i+j], type_of_elements[18*i+j], state_of_elements[18*i+j], density_of_elements[18*i+j], electronegativity_of_elements[18*i+j]))
345                but.grid(row=i,column=j)
346                temp.append(but)
347            buttons.append(temp)
348
349        # Creating a 2x15 table of buttons for secondary elements
350        for i in range(2):
351            for j in range(15):
352                if i == 0: # If row 1 then different color
353                    tk.Button(sec_elem,text=sec[15*i+j],width=8,bg=rare_m_col, command=partial(PT, tk_window_2[15*i+j], name_of_elements_sec[15*i+j], sec[15*i+j], atomic_number_of_elements_sec[15*i+j], atomic_mass_of_elements_sec[15*i+j], block_of_elements_sec[15*i+j], group_no_of_elements_sec[15*i+j], period_no_of_elements_sec[15*i+j], type_of_elements_sec[15*i+j], state_of_elements_sec[15*i+j], density_of_elements_sec[15*i+j], electronegativity_of_elements_sec[15*i+j])).grid(row=i,column=j)
354                else:
355                    tk.Button(sec_elem,text=sec[15*i+j],width=8,bg=act_col, command=partial(PT, tk_window_2[15*i+j], name_of_elements_sec[15*i+j], sec[15*i+j], atomic_number_of_elements_sec[15*i+j], atomic_mass_of_elements_sec[15*i+j], block_of_elements_sec[15*i+j], group_no_of_elements_sec[15*i+j], period_no_of_elements_sec[15*i+j], type_of_elements_sec[15*i+j], state_of_elements_sec[15*i+j], density_of_elements_sec[15*i+j], electronegativity_of_elements_sec[15*i+j])).grid(row=i,column=j)
356
357
358    if __name__ == '__main__': # Setting colours for each button
359
360        # Manually pick out main elements from the table
361        non_metals     = buttons[0][0],buttons[1][12:16],buttons[2][13:16],buttons[3][14:16],buttons[4][15]
362        alk_metals     = [row[0] for row in buttons[1:]]
363        alk_ea_metals  = [row[1] for row in buttons[1:]]
364        halogens       = [row[16] for row in buttons[1:]]
365        noble_gases    = [row[-1] for row in buttons[:]]
366        transition_met = [buttons[x][3:12] for x in range(3,7)]
367        metals         = buttons[6][12:16],buttons[5][12:16],buttons[4][12:15],buttons[3][12:14],buttons[2][12]
368        rare_metals    = [row[2] for row in buttons[3:6]]
369        actinoid       = buttons[-1][2]
370        plain_but      = buttons[0][1:-1],buttons[1][2:12],buttons[2][2:12]
371
372        # Change colors for those main element buttons
373        actinoid['bg'] = act_col
374        for i in alk_metals:
375            i['bg'] = alk_m_col
376        for i in alk_ea_metals: 
377            i['bg'] = alk_ea_col
378        for i in halogens:      
379            i['bg'] = halogen_col
380        for i in noble_gases:   
381            i['bg'] = noble_g_col
382        for i in rare_metals:   
383            i['bg'] = rare_m_col
384
385        for i in transition_met:
386            for j in i:
387                j['bg'] = trans_m_col
388
389        for i in plain_but:
390            for j in i:
391                j['bg'] = plain_but_col
392
393        for i in non_metals:
394            if isinstance(i,list):
395                for j in i:
396                    j.config(bg=non_m_col)
397            else:
398                i.config(bg=non_m_col)
399
400        for i in metals:
401            if isinstance(i,list):
402                for j in i:
403                    j.config(bg=metals_col)
404            else:
405                i.config(bg=metals_col)
406        
407        for i in plain_but:
408            for j in i:
409                j['relief'] = 'flat'
410
411
412    if __name__ == '__main__': # Creating a frame for categorizing elements on basis of their colours 
413        legends_frame = tk.Canvas(root, bg='black', width=200, height=250)
414
415        tk.Label(legends_frame, text='Categories', bg='black', fg='white', font=['Bookman Old Style', 10, 'underline']).place(relx=0.25, rely=0.03)
416
417        labels = ['Actinides', 'Alkali Metals', 'Alkali Earth Metals', 'Halogens', 'Lanthanides', 'Metalloids', 'Noble Gases',  'Non Metals', 'Transition Metals']
418        colours = [act_col, alk_m_col, alk_ea_col, halogen_col, rare_m_col, metals_col, noble_g_col, non_m_col, trans_m_col]
419
420        for i in range(0,9):
421            tk.Label(legends_frame, text=labels[i], bg='black', fg=colours[i]).place(relx=0.4, rely=0.16+i*0.09)
422            tk.Canvas(legends_frame, bg=colours[i], width=32, height=12, borderwidth=0).place(relx=0.1, rely=0.16+i*0.09)
423
424        legends_frame.place(relx=0.8, rely=0.05)
425
426
427    tk.Button(root,text='EXIT',command=root.destroy).place(x=10, y=10)
428
429    root.mainloop()
430from tkinter import *
431
432period_1 = ['H' ,'','','','','','','','','','','','','','','','','He']
433period_2 = ['Li','Be','','','','','','','','','','','B','C','N','O','F','Ne']
434period_3 = ['Na','Mg','','','','','','','','','','','Al','Si','P','S','Cl','Ar']
435period_4 = &quot;&quot;&quot;K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr&quot;&quot;&quot;.split(&quot; &quot;)
436period_5 = &quot;&quot;&quot;Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe&quot;&quot;&quot;.split(&quot; &quot;)
437period_6 = &quot;&quot;&quot;Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn&quot;&quot;&quot;.split(&quot; &quot;)
438period_7 = &quot;&quot;&quot;Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og&quot;&quot;&quot;.split(&quot; &quot;)
439
440period_6a = &quot;&quot;&quot;La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu&quot;&quot;&quot;.split(&quot; &quot;)
441period_7a = &quot;&quot;&quot;Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr&quot;&quot;&quot;.split(&quot; &quot;)
442
443# Making a list of main elements and secondary elements
444main = period_1 + period_2 + period_3 + period_4 + period_5 + period_6 + period_7
445sec  = period_6a + period_7a
446
447# Colors for each group
448non_m_col   = '#feab90'
449alk_m_col   = '#ffe0b2'
450alk_ea_col  = '#fecc81'
451trans_m_col = '#d2c4e8'
452halogen_col = '#a4d7a7'
453metals_col  = '#feab90'
454noble_g_col = '#fefffe'
455act_col     = '#b2e5fd'
456rare_m_col  = '#e7ee9a' 
457
458root = Tk()
459
460# Frame for the entire table
461period_tab = Frame(root)
462period_tab.pack()
463
464# Frame for the main elements only
465main_elem = Frame(period_tab)
466main_elem.pack()
467
468# Frame for the secondary elements only
469sec_elem = Frame(period_tab)
470sec_elem.pack(pady=10)
471
472# Creating a 7x18 table of buttons and appending it to a 2D python list for main elements
473buttons = []
474for i in range(7):
475    temp = []
476    for j in range(18):
477        but = Button(main_elem,text=main[18*i+j],width=10,bg='#f0f0f0')
478        but.grid(row=i,column=j)
479        temp.append(but)
480    buttons.append(temp)
481
482# Creating a 2x15 table of buttons for secondary elements
483for i in range(2):
484    for j in range(15):
485        text = sec[15*i+j]
486        if i == 0: # If row 1 then different color
487            Button(sec_elem,text=text,width=10,bg=rare_m_col).grid(row=i,column=j)
488        else:
489            Button(sec_elem,text=text,width=10,bg=act_col).grid(row=i,column=j)
490            
491# Manually pick out main elements from the table
492non_metals     = buttons[0][0],buttons[1][12:16],buttons[2][13:16],buttons[3][14:16],buttons[4][15]
493alk_metals     = [row[0] for row in buttons[1:]]
494alk_ea_metals  = [row[1] for row in buttons[1:]]
495halogens       = [row[16] for row in buttons[1:]]
496noble_gases    = [row[-1] for row in buttons[:]]
497transition_met = [buttons[x][3:12] for x in range(3,7)]
498metals         = buttons[6][12:16],buttons[5][12:16],buttons[4][12:15],buttons[3][12:14],buttons[2][12]
499rare_metals    = [row[2] for row in buttons[3:6]]
500actinoid       = buttons[-1][2]
501plain_but      = buttons[0][1:-1],buttons[1][2:12],buttons[2][2:12]
502
503# Change colors for those main element buttons
504actinoid['bg'] = act_col
505for i in alk_metals:    i['bg'] = alk_m_col
506for i in alk_ea_metals: i['bg'] = alk_ea_col
507for i in halogens:      i['bg'] = halogen_col
508for i in noble_gases:   i['bg'] = noble_g_col
509for i in rare_metals:   i['bg'] = rare_m_col
510
511for i in transition_met:
512    for j in i:
513        j['bg'] = trans_m_col
514
515for i in non_metals:
516    if isinstance(i,list):
517        for j in i:
518            j.config(bg=non_m_col)
519    else:
520        i.config(bg=non_m_col)
521
522for i in metals:
523    if isinstance(i,list):
524        for j in i:
525            j.config(bg=metals_col)
526    else:
527        i.config(bg=metals_col)
528 
529for i in plain_but:
530    for j in i:
531        j['relief'] = 'flat'
532
533Button(root,text='EXIT',command=root.destroy).pack(pady=10)
534
535root.mainloop()
536

I've commented the code to make it more understandable. The slicing part might seem a bit complicated because python list does not support 2D slicing. One way is to create a numpy array and store the coordinates onto it and then retrieve the respective button object based on coordinate, might be longer code but it would make the slicing more easier and understandable as numpy supports 2D slicing.

Final output of the GUI: ss

Edit: Here is a more advanced and not so complicated periodic table

Source https://stackoverflow.com/questions/70506629

QUESTION

Android: Iterative queue-based flood fill algorithm 'expandToNeighborsWithMap()' function is unusually slow

Asked 2021-Dec-30 at 04:27

(Solution has been found, please avoid reading on.)

I am creating a pixel art editor for Android, and as for all pixel art editors, a paint bucket (fill tool) is a must need.

To do this, I did some research on flood fill algorithms online.

I stumbled across the following video which explained how to implement an iterative flood fill algorithm in your code. The code used in the video was JavaScript, but I was easily able to convert the code from the video to Kotlin:

https://www.youtube.com/watch?v=5Bochyn8MMI&t=72s&ab_channel=crayoncode

Here is an excerpt of the JavaScript code from the video:

enter image description here

Converted code:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29

Now, I want to address two major issues I'm having with the code of mine:

  • Performance

  • Flooding glitch (fixed by suggestion from person in the comments)


Performance

A flood fill needs to be very fast and shouldn't take less than a second, the problem is, say I have a canvas of size 50 x 50, and I decide to fill in the whole canvas, it can take up to 8 seconds or more.

Here is some data I've compiled for the time it's taken to fill in a whole canvas given the spanCount value:

spanCount approx time taken in seconds to fill whole canvas
10 <1 seconds
20 ~2 seconds
40 ~6 seconds
60 ~15 seconds
100 ~115 seconds

The conclusion from the data is that the flood fill algorithm is unusually slow.

To find out why, I decided to test out which parts of the code are taking the most time to compile. I came to the conclusion that the expandToNeighbors function is taking the most time out of all the other tasks:

enter image description here

Here is an excerpt of the expandToNeighbors function:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
30    var asIndex1 = from.x
31    var asIndex2 = from.x
32
33    var asIndex3 = from.y
34    var asIndex4 = from.y
35
36    if (from.x &gt; 1) {
37        asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38    }
39
40    if (from.x &lt; instance.spanCount) {
41        asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42    }
43
44    if (from.y &gt; 1) {
45        asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46    }
47
48    if (from.y &lt; instance.spanCount) {
49        asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
50    }
51
52    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
53} 
54

To understand the use of the expandToNeighbors function, I would recommend watching the video that I linked above.

(The if statements are there to make sure you won't get an IndexOutOfBoundsException if you try and expand from the edge of the canvas.)

This function will return the index of the north, south, west, and east pixels from the xyPositionData list which contains XYPosition objects.

(The black pixel is the from parameter.)

enter image description here

The xyPositionData list is initialized once in the convertXYDataToIndex function, here:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
30    var asIndex1 = from.x
31    var asIndex2 = from.x
32
33    var asIndex3 = from.y
34    var asIndex4 = from.y
35
36    if (from.x &gt; 1) {
37        asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38    }
39
40    if (from.x &lt; instance.spanCount) {
41        asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42    }
43
44    if (from.y &gt; 1) {
45        asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46    }
47
48    if (from.y &lt; instance.spanCount) {
49        asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
50    }
51
52    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
53} 
54var xyPositionData: List&lt;XYPosition&gt;? = null
55var rectangleData: List&lt;RectF&gt;? = null
56
57fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
58
59    if (rectangleData == null) {
60        rectangleData = instance.rectangles.keys.toList()
61    }
62
63    if (xyPositionData == null) {
64        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
65            rectangleData!!.size,
66            instance.spanCount.toInt()
67        )
68    }
69
70    return xyPositionData!!.indexOf(from)
71}
72

So, the code works fine (kind of) but the expandToNeighbors function is very slow, and it is the main reason why the flood fill algorithm is taking a long time.

My colleague suggested that indexOf may be slowing everything down, and that I should probably switch to a Map-based implementation with a key being XYPosition and a value being Int representing the index, so I replaced it with the following:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
30    var asIndex1 = from.x
31    var asIndex2 = from.x
32
33    var asIndex3 = from.y
34    var asIndex4 = from.y
35
36    if (from.x &gt; 1) {
37        asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38    }
39
40    if (from.x &lt; instance.spanCount) {
41        asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42    }
43
44    if (from.y &gt; 1) {
45        asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46    }
47
48    if (from.y &lt; instance.spanCount) {
49        asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
50    }
51
52    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
53} 
54var xyPositionData: List&lt;XYPosition&gt;? = null
55var rectangleData: List&lt;RectF&gt;? = null
56
57fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
58
59    if (rectangleData == null) {
60        rectangleData = instance.rectangles.keys.toList()
61    }
62
63    if (xyPositionData == null) {
64        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
65            rectangleData!!.size,
66            instance.spanCount.toInt()
67        )
68    }
69
70    return xyPositionData!!.indexOf(from)
71}
72fun expandToNeighborsWithMap(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
73    var asIndex1 = from.x
74    var asIndex2 = from.x
75
76    var asIndex3 = from.y
77    var asIndex4 = from.y
78
79    if (from.x &gt; 1) {
80        asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81    }
82
83    if (from.x &lt; instance.spanCount) {
84        asIndex2 =  rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85    }
86
87    if (from.y &gt; 1) {
88        asIndex3 =  rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89    }
90
91    if (from.y &lt; instance.spanCount) {
92        asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
93    }
94
95    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
96}
97

It functions the same way, only this time it uses a Map which is initialized here:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
30    var asIndex1 = from.x
31    var asIndex2 = from.x
32
33    var asIndex3 = from.y
34    var asIndex4 = from.y
35
36    if (from.x &gt; 1) {
37        asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38    }
39
40    if (from.x &lt; instance.spanCount) {
41        asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42    }
43
44    if (from.y &gt; 1) {
45        asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46    }
47
48    if (from.y &lt; instance.spanCount) {
49        asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
50    }
51
52    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
53} 
54var xyPositionData: List&lt;XYPosition&gt;? = null
55var rectangleData: List&lt;RectF&gt;? = null
56
57fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
58
59    if (rectangleData == null) {
60        rectangleData = instance.rectangles.keys.toList()
61    }
62
63    if (xyPositionData == null) {
64        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
65            rectangleData!!.size,
66            instance.spanCount.toInt()
67        )
68    }
69
70    return xyPositionData!!.indexOf(from)
71}
72fun expandToNeighborsWithMap(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
73    var asIndex1 = from.x
74    var asIndex2 = from.x
75
76    var asIndex3 = from.y
77    var asIndex4 = from.y
78
79    if (from.x &gt; 1) {
80        asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81    }
82
83    if (from.x &lt; instance.spanCount) {
84        asIndex2 =  rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85    }
86
87    if (from.y &gt; 1) {
88        asIndex3 =  rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89    }
90
91    if (from.y &lt; instance.spanCount) {
92        asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
93    }
94
95    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
96}
97var xyPositionData: List&lt;XYPosition&gt;? = null
98var rectangleData: List&lt;RectF&gt;? = null
99var rectangleDataMap: Map&lt;XYPosition, Int&gt;? = null
100
101fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
102
103    if (rectangleData == null) {
104        rectangleData = instance.rectangles.keys.toList()
105    }
106
107    if (xyPositionData == null) {
108        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
109            rectangleData!!.size,
110            instance.spanCount.toInt()
111        )
112    }
113
114    if (rectangleDataMap == null) {
115        rectangleDataMap = MathExtensions.convertListToMap(
116            rectangleData!!.size,
117            instance.spanCount.toInt()
118        )
119    }
120
121    return xyPositionData!!.indexOf(from)
122}
123

Converting the code to use a map increased the speed by around 20%, although the algorithm is still slow.

After spending a couple of days trying to make the algorithm work faster, I'm out of ideas and I'm unsure why the expandToNeighbors function is taking a long time. Any help would be appreciated to fix this issue.

Apologies if I didn't do a good enough job of explaining the exact issue, but I have tried my best. Implementation-wise it is quite messy unfortunately because of the whole list index to XYPosition conversions, but at least it works - the only problem is the performance.


So I have two one major problem, if anyone can try and find a solution for it, it would be great because I have tried to myself without much luck.

I've actually pushed the fill tool to GitHub as a KIOL (Known Issue or Limitation), so the user can use the fill tool if they want, but they need to be aware of the limitations/issues. This is so anyone who wants to help me fix this can have a look at my code and reproduce the bugs.

Link to repository:

https://github.com/realtomjoney/PyxlMoose


Edit after bounty

I understand that this question is extremely difficult to answer and will require a lot of thinking. I've tried myself to fix these issues but haven't had much success, so I'm offering 50 reputation for anyone who can assist.

I would recommend you clone PyxlMoose and reproduce the errors, then work from there. Relying on the code snippets isn't enough.


Formula for converting XY position to an index

Somebody in the comments suggested a formula for converting an XYPosition to an index value, I came up with the following method which works:

1Tools.FILL_TOOL -&gt; {
2            val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4            val queue = LinkedList&lt;XYPosition&gt;()
5
6            queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8            val selectedColor = getSelectedColor()
9
10            while (queue.isNotEmpty() &amp;&amp; seedColor != selectedColor) { // While the queue is not empty the code below will run
11                val current = queue.poll()
12                val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
13
14                if (color != seedColor) {
15                    continue
16                }
17
18                instance.extraCanvas.apply {
19                    instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
20                    drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
21
22                    for (index in expandToNeighborsWithMap(instance, current)) {
23                        val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
24                        queue.offer(candidate)
25                    }
26                }
27            }
28        }
29fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
30    var asIndex1 = from.x
31    var asIndex2 = from.x
32
33    var asIndex3 = from.y
34    var asIndex4 = from.y
35
36    if (from.x &gt; 1) {
37        asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38    }
39
40    if (from.x &lt; instance.spanCount) {
41        asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42    }
43
44    if (from.y &gt; 1) {
45        asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46    }
47
48    if (from.y &lt; instance.spanCount) {
49        asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
50    }
51
52    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
53} 
54var xyPositionData: List&lt;XYPosition&gt;? = null
55var rectangleData: List&lt;RectF&gt;? = null
56
57fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
58
59    if (rectangleData == null) {
60        rectangleData = instance.rectangles.keys.toList()
61    }
62
63    if (xyPositionData == null) {
64        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
65            rectangleData!!.size,
66            instance.spanCount.toInt()
67        )
68    }
69
70    return xyPositionData!!.indexOf(from)
71}
72fun expandToNeighborsWithMap(instance: MyCanvasView, from: XYPosition): List&lt;Int&gt; {
73    var asIndex1 = from.x
74    var asIndex2 = from.x
75
76    var asIndex3 = from.y
77    var asIndex4 = from.y
78
79    if (from.x &gt; 1) {
80        asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81    }
82
83    if (from.x &lt; instance.spanCount) {
84        asIndex2 =  rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85    }
86
87    if (from.y &gt; 1) {
88        asIndex3 =  rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89    }
90
91    if (from.y &lt; instance.spanCount) {
92        asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
93    }
94
95    return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
96}
97var xyPositionData: List&lt;XYPosition&gt;? = null
98var rectangleData: List&lt;RectF&gt;? = null
99var rectangleDataMap: Map&lt;XYPosition, Int&gt;? = null
100
101fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
102
103    if (rectangleData == null) {
104        rectangleData = instance.rectangles.keys.toList()
105    }
106
107    if (xyPositionData == null) {
108        xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
109            rectangleData!!.size,
110            instance.spanCount.toInt()
111        )
112    }
113
114    if (rectangleDataMap == null) {
115        rectangleDataMap = MathExtensions.convertListToMap(
116            rectangleData!!.size,
117            instance.spanCount.toInt()
118        )
119    }
120
121    return xyPositionData!!.indexOf(from)
122}
123    fun convertXYPositionToIndex(xyPosition: XYPosition, spanCount: Int): Int {
124        val positionX = xyPosition.x
125        val positionY = xyPosition.y
126
127        return (spanCount - positionY) + (spanCount * (positionX - 1))
128    }
129

The only problem is - it increases the speed by around 50% but it's still taking around 10-15 seconds to fill in an area of 80 by 80 pixels, so it has helped to a large degree although it's still very slow. But thank you very much for the suggestion anyways, it has helped a lot :)

ANSWER

Answered 2021-Dec-29 at 08:28

I think the performance issue is because of expandToNeighbors method generates 4 points all the time. It becomes crucial on the border, where you'd better generate 3 (or even 2 on corner) points, so extra point is current position again. So first border point doubles following points count, second one doubles it again (now it's x4) and so on.

If I'm right, you saw not the slow method work, but it was called too often.

Source https://stackoverflow.com/questions/70490938

QUESTION

create a circle object and push them in array

Asked 2021-Dec-30 at 04:14

I need to create circles in canvas using fabric. Every click, there is a circle created. However, if the new circle created it will replace old circle. This my stackblitz demo.

HTML

1&lt;canvas #canvas id=&quot;canvas&quot; width=&quot;900&quot; height=&quot;400&quot; &gt;
2  &lt;p&gt;Your browser doesn't support canvas!&lt;/p&gt;
3&lt;/canvas&gt;
4

TS

1&lt;canvas #canvas id=&quot;canvas&quot; width=&quot;900&quot; height=&quot;400&quot; &gt;
2  &lt;p&gt;Your browser doesn't support canvas!&lt;/p&gt;
3&lt;/canvas&gt;
4    this.canvas = new fabric.Canvas('canvas');
5    var circle = new fabric.Circle({
6    radius: 20,
7    fill: '#eef',
8    originX: 'center',
9    originY: 'center'
10});
11    var text = new fabric.Text(`${data.data.name}`, {
12    fontSize: 30,
13    originX: 'center',
14    originY: 'center'
15});
16    this.group = new fabric.Group([circle, text], {
17    left: event.e.offsetX,
18    top: event.e.offsetY,
19    angle: 0
20});
21console.log(this.group);
22this.canvas.add(this.group);
23
24this.canvas.setActiveObject(this.canvas.item[0]);
25this.canvas.renderAll();
26

ANSWER

Answered 2021-Dec-30 at 04:14

The problem is that you're repeatedly creating the Canvas object, which is likely resulting in the canvas element being drawn over multiple times in separate instances. That is, every new instance will only ever contain the most recent circle and will draw over the previous instance. What you want to do is create the instance once and then reference that instance each time moving forward.

In your code snippet above, it could look something like this:

1&lt;canvas #canvas id=&quot;canvas&quot; width=&quot;900&quot; height=&quot;400&quot; &gt;
2  &lt;p&gt;Your browser doesn't support canvas!&lt;/p&gt;
3&lt;/canvas&gt;
4    this.canvas = new fabric.Canvas('canvas');
5    var circle = new fabric.Circle({
6    radius: 20,
7    fill: '#eef',
8    originX: 'center',
9    originY: 'center'
10});
11    var text = new fabric.Text(`${data.data.name}`, {
12    fontSize: 30,
13    originX: 'center',
14    originY: 'center'
15});
16    this.group = new fabric.Group([circle, text], {
17    left: event.e.offsetX,
18    top: event.e.offsetY,
19    angle: 0
20});
21console.log(this.group);
22this.canvas.add(this.group);
23
24this.canvas.setActiveObject(this.canvas.item[0]);
25this.canvas.renderAll();
26// Ensures that the instance is only created once!
27if(!this.canvas) {
28    this.canvas = new fabric.Canvas('canvas');
29}
30
31var circle = new fabric.Circle({
32    radius: 20,
33    fill: '#eef',
34    originX: 'center',
35    originY: 'center'
36});
37
38var text = new fabric.Text(`${data.data.name}`, {
39    fontSize: 30,
40    originX: 'center',
41    originY: 'center'
42});
43
44this.group = new fabric.Group([circle, text], {
45    left: event.e.offsetX,
46    top: event.e.offsetY,
47    angle: 0
48});
49
50console.log(this.group);
51this.canvas.add(this.group);
52
53this.canvas.setActiveObject(this.canvas.item[0]);
54this.canvas.renderAll();
55

I've even added relevant changes to your stackblitz project in a fork to help illustrate how this could be accomplished.

Source https://stackoverflow.com/questions/70497711

Community Discussions contain sources that include Stack Exchange Network

Tutorials and Learning Resources in Canvas

Tutorials and Learning Resources are not available at this moment for Canvas

Share this Page

share link

Get latest updates on Canvas