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
by fabricjs javascript
21561 NOASSERTION
Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
by Automattic c++
8313
Node canvas is a Cairo backed Canvas implementation for NodeJS.
by szimek typescript
7897 MIT
HTML5 canvas based smooth signature drawing
by tsayen javascript
7728 NOASSERTION
Generates an image from a DOM node using HTML5 canvas
by antvis typescript
7660 MIT
📱📈An elegant, interactive and flexible charting library for mobile.
by piskelapp javascript
7530 Apache-2.0
A simple web-based tool for Spriting and Pixel art.
by bfirsh javascript
5431 Apache-2.0
A JavaScript NES emulator.
by instructure ruby
4363 NOASSERTION
The open LMS by Instructure, Inc.
by soulwire javascript
3890 MIT
Cross-Platform JavaScript Creative Coding Framework
Trending New libraries in Canvas
by steveruizok typescript
1735 MIT
Draw perfect pressure-sensitive freehand strokes.
by samizdatco javascript
956 MIT
A canvas environment for Node.js
by Brooooooklyn rust
751 MIT
High performance skia binding to Node.js. Zero system dependencies and pure npm packages without any postinstall scripts nor node-gyp.
by HashLips javascript
640 MIT
Create generative art by using the canvas api and node js
by gmrchk typescript
590 NOASSERTION
The cursor is the heart of any interaction with the web. Why not take it to the next level? 🚀
by Gitjinfeiyang javascript
467 MIT
使用render函数在canvas中创建文档流布局,小程序海报图、小程序朋友圈分享图。easy-canvas is a powerful tool helps us easy to layout with canvas.
by HashLips javascript
442 MIT
Create generative art by using the canvas api and node js, feel free to contribute to this repo with new ideas.
by jdxyw go
440 MIT
Generative Art in Go
by idrawjs typescript
432 MIT
A simple JavaScript framework for Drawing on the web.(一个面向Web绘图的JavaScript框架)
Top Authors in Canvas
1
10 Libraries
301
2
10 Libraries
181
3
8 Libraries
167
4
7 Libraries
60
5
7 Libraries
277
6
6 Libraries
204
7
6 Libraries
148
8
6 Libraries
109
9
6 Libraries
4922
10
5 Libraries
18
1
10 Libraries
301
2
10 Libraries
181
3
8 Libraries
167
4
7 Libraries
60
5
7 Libraries
277
6
6 Libraries
204
7
6 Libraries
148
8
6 Libraries
109
9
6 Libraries
4922
10
5 Libraries
18
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:
- Start by installing the necessary dependencies through package managers like npm.
- Follow the library's documentation to set up the components and understand the API.
- 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.
- The solution is created and tested using Vscode 1.77.2 version
- 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
- For any support on kandi solution kits, please use the chat
- 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:09When 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 < 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 < 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:09Coming 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 < 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 > 0.0)
46 {
47 if (time_constant > 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 < 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 > 0.0)
46 {
47 if (time_constant > 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 < 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 > 0.0)
46 {
47 if (time_constant > 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 < 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 > 0.0)
46 {
47 if (time_constant > 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 > 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 < 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 > 0.0)
46 {
47 if (time_constant > 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 > 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 /// <summary>
79 /// Initializes first-order filter
80 /// </summary>
81 /// <param name="value">initial value of filter</param>
82 /// <param name="time_constant">time constant of filter, in seconds</param>
83 /// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> must be positive</exception>
84 public ExpFilter(double value, double time_constant)
85 {
86 // time constant must be positive
87 if (time_constant <= 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 /// <summary>
104 /// Initializes second-order filter
105 /// </summary>
106 /// <param name="value">initial value of filter</param>
107 /// <param name="time_constant">time constant of primary filter, in seconds</param>
108 /// <param name="dot_time_constant">time constant of secondary filter, in seconds</param>
109 /// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> and <paramref name="dot_time_constant"/> must be positive</exception>
110 public ExpFilter(double value, double time_constant, double dot_time_constant)
111 {
112 // time constant must be positive
113 if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
114 if (dot_time_constant <= 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 /// <summary>
158 /// Get/Set filter value
159 /// </summary>
160 public double Value
161 {
162 get => _output;
163 set => _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 >= 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
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:42I 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 UIView
s 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
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<UITouch>, 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("""
113 Element Frame = \(frame)
114 Element Bounds = \(bounds)
115 Element Center = \(center)
116 """)
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:03Here 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<UITouch>, 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("""
113 Element Frame = \(frame)
114 Element Bounds = \(bounds)
115 Element Center = \(center)
116 """)
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
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<UITouch>, 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("""
113 Element Frame = \(frame)
114 Element Bounds = \(bounds)
115 Element Center = \(center)
116 """)
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
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<UITouch>, 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("""
113 Element Frame = \(frame)
114 Element Bounds = \(bounds)
115 Element Center = \(center)
116 """)
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:
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
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<UITouch>, 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("""
113 Element Frame = \(frame)
114 Element Bounds = \(bounds)
115 Element Center = \(center)
116 """)
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 ?
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.
QUESTION
Prevent y-axis labels from being cut off
Asked 2022-Jan-31 at 04:00My 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, '$&,').replace(/[,.]/g, m => (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, '$&,').replace(/[,.]/g, m => (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, '$&,').replace(/[,.]/g, m => (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}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
117<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
118<div class="card card-chart">
119 <div class="card-header">
120 <div class="row">
121 <div class="col-sm-6 text-left">
122 <h5 class="card-category">Totale vendite</h5>
123 <h2 class="card-title">Totali</h2>
124 </div>
125 </div>
126 </div>
127 <div class="card-body">
128 <div class="chart-area">
129 <canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"></canvas>
130 </div>
131 </div>
132</div>
ANSWER
Answered 2022-Jan-26 at 16:52The 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, '$&,').replace(/[,.]/g, m => (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}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
117<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
118<div class="card card-chart">
119 <div class="card-header">
120 <div class="row">
121 <div class="col-sm-6 text-left">
122 <h5 class="card-category">Totale vendite</h5>
123 <h2 class="card-title">Totali</h2>
124 </div>
125 </div>
126 </div>
127 <div class="card-body">
128 <div class="chart-area">
129 <canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"></canvas>
130 </div>
131 </div>
132</div>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, '$&,').replace(/[,.]/g, m => (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, '$&,').replace(/[,.]/g, m => (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}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
117<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
118<div class="card card-chart">
119 <div class="card-header">
120 <div class="row">
121 <div class="col-sm-6 text-left">
122 <h5 class="card-category">Totale vendite</h5>
123 <h2 class="card-title">Totali</h2>
124 </div>
125 </div>
126 </div>
127 <div class="card-body">
128 <div class="chart-area">
129 <canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"></canvas>
130 </div>
131 </div>
132</div>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, '$&,').replace(/[,.]/g, m => (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, '$&,').replace(/[,.]/g, m => (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}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
117<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
118<div class="card card-chart">
119 <div class="card-header">
120 <div class="row">
121 <div class="col-sm-6 text-left">
122 <h5 class="card-category">Totale vendite</h5>
123 <h2 class="card-title">Totali</h2>
124 </div>
125 </div>
126 </div>
127 <div class="card-body">
128 <div class="chart-area">
129 <canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"></canvas>
130 </div>
131 </div>
132</div>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, '$&,').replace(/[,.]/g, m => (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}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
247<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
248<div class="card card-chart">
249 <div class="card-header">
250 <div class="row">
251 <div class="col-sm-6 text-left">
252 <h5 class="card-category">Totale vendite</h5>
253 <h2 class="card-title">Totali</h2>
254 </div>
255 </div>
256 </div>
257 <div class="card-body">
258 <div class="chart-area">
259 <canvas id="chartTotali" width="1563" height="220" style="display: block; box-sizing: border-box; height: 220px; width: 1563px;"></canvas>
260 </div>
261 </div>
262</div>
QUESTION
Javascript: frame precise video stop
Asked 2022-Jan-28 at 14:55I 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("Automatic stop: found stop frame.");
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):
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):
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:18The video has frame rate of 25fps, and not 24fps:
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("Automatic stop: found stop frame.");
8 pauseMyVideo();
9 // Ensure we are on the proper frame.
10 video.seekTo({frame: curr_frame});
11 }
12 }
13 });
14 const callback = (now, metadata) => {
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.
QUESTION
Opening PDFs in WebView2 based on selection in CheckBoxColumn
Asked 2022-Jan-19 at 14:26So,
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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
39
40 //} else {
41
42 // return;
43 //}
44
45 } catch (Exception ex) {
46
47 MessageBox.Show(ex.Message);
48 }
49 }
50}
51<Wpf:WebView2
52 x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
53 Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
54 Source="{Binding PdfFilePath}"></Wpf:WebView2>
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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
39
40 //} else {
41
42 // return;
43 //}
44
45 } catch (Exception ex) {
46
47 MessageBox.Show(ex.Message);
48 }
49 }
50}
51<Wpf:WebView2
52 x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
53 Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
54 Source="{Binding PdfFilePath}"></Wpf:WebView2>
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("You must chose an invoice to preview first.");
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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
39
40 //} else {
41
42 // return;
43 //}
44
45 } catch (Exception ex) {
46
47 MessageBox.Show(ex.Message);
48 }
49 }
50}
51<Wpf:WebView2
52 x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
53 Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
54 Source="{Binding PdfFilePath}"></Wpf:WebView2>
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("You must chose an invoice to preview first.");
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:26I 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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
39
40 //} else {
41
42 // return;
43 //}
44
45 } catch (Exception ex) {
46
47 MessageBox.Show(ex.Message);
48 }
49 }
50}
51<Wpf:WebView2
52 x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
53 Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
54 Source="{Binding PdfFilePath}"></Wpf:WebView2>
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("You must chose an invoice to preview first.");
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("You must chose an invoice to preview first.");
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<PaidTrip> paidTrips = PaidTrips.ToList();
8
9 tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
10
11 foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
12
13 /* SQL SERVER CODE
14 * SQL SERVER CODE
15 * SQL SERVER CODE */
16
17 List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
18
19 IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
20
21 foreach (IGrouping<string, PaidTrip> 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 = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
32 string filePath = $@"C:\Users\{userName}\AppData\Local\Temp\";
33
34 PdfFilePath = $"{filePath}{fileName}";
35
36 //if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
37
38 document.Save($"{PdfFilePath}");
39
40 //} else {
41
42 // return;
43 //}
44
45 } catch (Exception ex) {
46
47 MessageBox.Show(ex.Message);
48 }
49 }
50}
51<Wpf:WebView2
52 x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
53 Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
54 Source="{Binding PdfFilePath}"></Wpf:WebView2>
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("You must chose an invoice to preview first.");
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("You must chose an invoice to preview first.");
95 }
96}
97
98if (LicenseHolderID == PreviewInvoiceViewModel.PreviewedInvoice) {
99 document.Save($"{fullPath}");
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..
QUESTION
Is it possible to manually update the value of a Behaviour? (Functional Reactive Programming, Threepenny)
Asked 2022-Jan-17 at 16:02I 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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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:02Full 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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords <- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords <@ 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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords <- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords <@ 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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords <- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords <@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -> [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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords <- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords <@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -> [Integer])
43listState <- 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 <$ UI.click redButton
4colorBlueSelected = const ColorBlue <$ 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 <- accumB ColorRed modeEvent
11
12-- create a Behaviour
13mouseCoordinate <- 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 <- accumB ([1,2,3,4]) never
19
20-- Combine the Behaviours, we now have a tuple (chosenColorMode, mouseCoordinateTuple, savedList)
21
22let choices = (,,) <$> colorMode <*> mouseCoordinate <*> 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 <@ UI.click canvas
28
29onEvent makeChoice $ \(colorMode, (x,y), savedList) -> 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) -> [Integer] -> [Integer]
35updateMyList (x, y) oldList = ...
36mouseCoords :: Behavior (Double, Double)
37mouseCoords <- stepper (0,0) $ UI.mousemove canvas
38
39mouseClicked :: Event (Double, Double)
40mouseClicked = mouseCoords <@ UI.click canvas -- this is the Event we need
41listChangeEvent = fmap updateMyList mouseClicked
42listChangeEvent :: Event ([Integer] -> [Integer])
43listState <- 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 listChangeEvent
triggers, 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
.
QUESTION
Problem resizing plot on tkinter figure canvas
Asked 2022-Jan-15 at 02:30Python 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["figure.figsize"] = [18,10]
9
10root=Tk()
11root.wm_title("Root Window")
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:23try 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["figure.figsize"] = [18,10]
9
10root=Tk()
11root.wm_title("Root Window")
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 % [
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 = ["Name : ", "Atomic No. : ", "Atomic Mass : ", "Block : ", "Group No. : ", "Period No. : ", "Type : ", "State : ", "Density : ", "Electronegativity : "]
24
25 scroll = tk.Scrollbar(root, orient="horizontal")
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="x", 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"{texts[x]}{i[x]}", 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"Number of resuts : {results}", 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 = ["Name : ", "Atomic No. : ", "Atomic Mass : ", "Block : ", "Group No. : ", "Period No. : ", "Type : ", "State : ", "Density : ", "Electronegativity : "]
24
25 scroll = tk.Scrollbar(root, orient="horizontal")
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="x", 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"{texts[x]}{i[x]}", 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"Number of resuts : {results}", 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(" ")
196 name_of_elements.append(" ")
197 atomic_mass_of_elements.append(" ")
198 block_of_elements.append(" ")
199 group_no_of_elements.append(" ")
200 period_no_of_elements.append(" ")
201 type_of_elements.append(" ")
202 state_of_elements.append(" ")
203 density_of_elements.append(" ")
204 electronegativity_of_elements.append(" ")
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 = """K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr""".split(" ")
262 period_5 = """Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe""".split(" ")
263 period_6 = """Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn""".split(" ")
264 period_7 = """Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og""".split(" ")
265
266 period_6a = """La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu""".split(" ")
267 period_7a = """Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr""".split(" ")
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="Periodic Table of Elements", 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"Symbol : {symbol}", bg='black', fg='white').place(relx=0.05, rely=0.3)
326 tk.Label(window, text=f"Atomic Number : {atom_no}", bg='black', fg='white').place(relx=0.05, rely=0.35)
327 tk.Label(window, text=f"Atomic Mass : {atom_mass}", bg='black', fg='white').place(relx=0.05, rely=0.4)
328 tk.Label(window, text=f"Block : {block}", bg='black', fg='white').place(relx=0.05, rely=0.45)
329 tk.Label(window, text=f"Group Number : {group}", bg='black', fg='white').place(relx=0.05, rely=0.5)
330 tk.Label(window, text=f"Period Number : {period}", bg='black', fg='white').place(relx=0.05, rely=0.55)
331 tk.Label(window, text=f"Type of element : {type}", bg='black', fg='white').place(relx=0.05, rely=0.6)
332 tk.Label(window, text=f"State of element : {state}", bg='black', fg='white').place(relx=0.05, rely=0.65)
333 tk.Label(window, text=f"Density : {density}", bg='black', fg='white').place(relx=0.05, rely=0.7)
334 tk.Label(window, text=f"Electronegativity : {electronegativity}", 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:33I 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 = ["Name : ", "Atomic No. : ", "Atomic Mass : ", "Block : ", "Group No. : ", "Period No. : ", "Type : ", "State : ", "Density : ", "Electronegativity : "]
24
25 scroll = tk.Scrollbar(root, orient="horizontal")
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="x", 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"{texts[x]}{i[x]}", 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"Number of resuts : {results}", 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(" ")
196 name_of_elements.append(" ")
197 atomic_mass_of_elements.append(" ")
198 block_of_elements.append(" ")
199 group_no_of_elements.append(" ")
200 period_no_of_elements.append(" ")
201 type_of_elements.append(" ")
202 state_of_elements.append(" ")
203 density_of_elements.append(" ")
204 electronegativity_of_elements.append(" ")
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 = """K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr""".split(" ")
262 period_5 = """Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe""".split(" ")
263 period_6 = """Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn""".split(" ")
264 period_7 = """Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og""".split(" ")
265
266 period_6a = """La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu""".split(" ")
267 period_7a = """Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr""".split(" ")
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="Periodic Table of Elements", 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"Symbol : {symbol}", bg='black', fg='white').place(relx=0.05, rely=0.3)
326 tk.Label(window, text=f"Atomic Number : {atom_no}", bg='black', fg='white').place(relx=0.05, rely=0.35)
327 tk.Label(window, text=f"Atomic Mass : {atom_mass}", bg='black', fg='white').place(relx=0.05, rely=0.4)
328 tk.Label(window, text=f"Block : {block}", bg='black', fg='white').place(relx=0.05, rely=0.45)
329 tk.Label(window, text=f"Group Number : {group}", bg='black', fg='white').place(relx=0.05, rely=0.5)
330 tk.Label(window, text=f"Period Number : {period}", bg='black', fg='white').place(relx=0.05, rely=0.55)
331 tk.Label(window, text=f"Type of element : {type}", bg='black', fg='white').place(relx=0.05, rely=0.6)
332 tk.Label(window, text=f"State of element : {state}", bg='black', fg='white').place(relx=0.05, rely=0.65)
333 tk.Label(window, text=f"Density : {density}", bg='black', fg='white').place(relx=0.05, rely=0.7)
334 tk.Label(window, text=f"Electronegativity : {electronegativity}", 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 = """K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr""".split(" ")
436period_5 = """Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe""".split(" ")
437period_6 = """Cs Ba * Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn""".split(" ")
438period_7 = """Fr Ra ** Rf D Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og""".split(" ")
439
440period_6a = """La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu""".split(" ")
441period_7a = """Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr""".split(" ")
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.
Edit: Here is a more advanced and not so complicated periodic table
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:
Converted code:
1Tools.FILL_TOOL -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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:
Here is an excerpt of the expandToNeighbors
function:
1Tools.FILL_TOOL -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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<Int> {
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 > 1) {
37 asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38 }
39
40 if (from.x < instance.spanCount) {
41 asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42 }
43
44 if (from.y > 1) {
45 asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46 }
47
48 if (from.y < 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.)
The xyPositionData
list is initialized once in the convertXYDataToIndex
function, here:
1Tools.FILL_TOOL -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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<Int> {
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 > 1) {
37 asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38 }
39
40 if (from.x < instance.spanCount) {
41 asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42 }
43
44 if (from.y > 1) {
45 asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46 }
47
48 if (from.y < 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<XYPosition>? = null
55var rectangleData: List<RectF>? = 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 -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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<Int> {
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 > 1) {
37 asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38 }
39
40 if (from.x < instance.spanCount) {
41 asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42 }
43
44 if (from.y > 1) {
45 asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46 }
47
48 if (from.y < 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<XYPosition>? = null
55var rectangleData: List<RectF>? = 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<Int> {
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 > 1) {
80 asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81 }
82
83 if (from.x < instance.spanCount) {
84 asIndex2 = rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85 }
86
87 if (from.y > 1) {
88 asIndex3 = rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89 }
90
91 if (from.y < 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 -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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<Int> {
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 > 1) {
37 asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38 }
39
40 if (from.x < instance.spanCount) {
41 asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42 }
43
44 if (from.y > 1) {
45 asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46 }
47
48 if (from.y < 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<XYPosition>? = null
55var rectangleData: List<RectF>? = 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<Int> {
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 > 1) {
80 asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81 }
82
83 if (from.x < instance.spanCount) {
84 asIndex2 = rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85 }
86
87 if (from.y > 1) {
88 asIndex3 = rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89 }
90
91 if (from.y < instance.spanCount) {
92 asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
93 }
94
95 return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
96}
97var xyPositionData: List<XYPosition>? = null
98var rectangleData: List<RectF>? = null
99var rectangleDataMap: Map<XYPosition, Int>? = 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 -> {
2 val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
3
4 val queue = LinkedList<XYPosition>()
5
6 queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
7
8 val selectedColor = getSelectedColor()
9
10 while (queue.isNotEmpty() && 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<Int> {
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 > 1) {
37 asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
38 }
39
40 if (from.x < instance.spanCount) {
41 asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
42 }
43
44 if (from.y > 1) {
45 asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
46 }
47
48 if (from.y < 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<XYPosition>? = null
55var rectangleData: List<RectF>? = 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<Int> {
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 > 1) {
80 asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
81 }
82
83 if (from.x < instance.spanCount) {
84 asIndex2 = rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
85 }
86
87 if (from.y > 1) {
88 asIndex3 = rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
89 }
90
91 if (from.y < instance.spanCount) {
92 asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
93 }
94
95 return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
96}
97var xyPositionData: List<XYPosition>? = null
98var rectangleData: List<RectF>? = null
99var rectangleDataMap: Map<XYPosition, Int>? = 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:28I 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.
QUESTION
create a circle object and push them in array
Asked 2021-Dec-30 at 04:14I 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<canvas #canvas id="canvas" width="900" height="400" >
2 <p>Your browser doesn't support canvas!</p>
3</canvas>
4
TS
1<canvas #canvas id="canvas" width="900" height="400" >
2 <p>Your browser doesn't support canvas!</p>
3</canvas>
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:14The 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<canvas #canvas id="canvas" width="900" height="400" >
2 <p>Your browser doesn't support canvas!</p>
3</canvas>
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.
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