Why is Flex SolidColorStroke acting that weird? - actionscript-3

I had to solve a problem concerning the SolidColorStroke of the flex framework. The scenario is simple, we have an visible object and we want a border around it. I built a graphics component that draws a Rect. That Rect was defined as follows
object; // the object which should get the border, defined somewhere outside,
// just fyi
var borderThickness:Number = 10;
rect.x = object.x - borderThickness;
rect.y = object.y - borderThickness;
rect.width = object.width + (borderThickness * 2);
rect.height = object.height + (borderThickness * 2);
rectStroke.weight = borderThickness;
//the MXML code
<s:Rect id="rect">
<s:stroke>
<s:SolidColorStroke id="rectStroke" />
</s:stroke>
</s:Rect>
I thought it should like this (just for illustration, not an exact image of what it should be, the border in this illustration is exactly around the object)
But I was wrong because the border did cover some parts of the objects on the right and on the bottom. My next thought was, that the stroke of the border doesn't grow inside the component, but on each side of the border in equally parts. What I mean by that is that when it grows inside the component and you place it at the location x = 0, y = 0 the width in my example of the left side of the border would be from 0 to 10. The thicker the border gets, the more it grows to the inside until there is just one big rectangle.
When I say it grows equally to each part of the border I mean that if you place the rect on x = 0, y = 0 and your border is 10px thick, the left side of the border goes from -5 to 5.
I hope it's clear what I mean.
So, as I said before I thought the border grows equally to both parts of the stroke. So I changed the calculations of the width and height to:
rect.width = object.width + borderThickness;
rect.height = object.height + borderThickness;
Now the width and height of the object is just increase the borderThickness (half of the borderThickness on every side). I thought now the border should fit exactly the object (as I expected it with my first version too...).
It look better than the first version but still some parts on the right and the left of the object are covered.
After a long time of thinking why it doesn't work as I expect I found a solution that worked for me. It seems that the stroke doesn't grow to equal parts on both sides, it seems that it grows by 75% to the inside and 25% to the outside. The following illustration shows what I mean by that.
The yellow line inside the border shows the actual border (when the stroke would be 1px). You can see that it is now exactly in the middle of the stroke but 75% from the inside and 25% from the outside.
My Question is, does anybody experience a similar behavior? Does anyone know why that works the way it does. Am I using it correct? Or what am I doing wrong?
The documentation of Adobe doesn't really tell you that the SolidColorStroke works this way. If you need more code, please let me know.
kind regards
Markus

<s:Group width="250" height="250" x="50" y="50">
<!--Normally borders grows inside so for ex: top should be equal to -weight of the stroke -->
<s:Rect top="-10" left="-10" right="-10" bottom="-10">
<s:stroke>
<s:SolidColorStroke weight="10" color="0x00FF00"/>
</s:stroke>
</s:Rect>
<s:Rect top="0" left="0" right="0" bottom="0">
<s:stroke>
<s:SolidColorStroke weight="1"/>
</s:stroke>
</s:Rect>
</s:Group>
AS Method:
private function drawSimpleBorders(obj:UIComponent, tickness:Number = 5):void
{
var gr:Graphics = obj.graphics;
gr.lineStyle(tickness,0,alpha);
var k:Number = tickness/2;
gr.drawRect(-k,-k, obj.width+tickness, obj.height+tickness);
}
I tried this and had no problems, just send target and you'll get borders.

Related

Centering on a specific point when zooming using SVG viewBox with arbitrary initial state

I have a SVG HTML element and I have implemented panning and zooming into it using the mouse. The current implementation of the zooming functionality just multiplies the original width and height of the element by a number that changes when the user scrolls the mouse.
This implementation preserves the origin (0,0) and all other points appear to move closer/further away from it depending on the direction of the zoom.
Intuitively and based o this question. I know, that If I want to zoom in/out on the point the mouse is currently pointing at, I have to pan the viewBox.
I have already looked at the linked questio, as well as two otheres, but I was unable to successfully apply the suggested solutions to my problem. I have also tried to derive the correct formula multiple times, but all my attempts so far have failed.
I am most likely missunderstanding something about the problem and I seem to be unable to generalise the existing answers to my problem.
The following values represent the current state of my viewBox:
offsetX
offsetY
scroll
width
height
I compute the zoomFactor as a function of the scroll variable (Math.exp(scroll/1000)) and set the viewBox property of my SVG as follows: `${offsetX} ${offsetY} ${width * zoomFactor} ${height * zoomFactor}`.
What I am struggling with, is computing the new offsetX and offsetY values based on the previous state and the current position of the mouse inside of the SVG.
processMouseScroll(event: WheelEvent) {
const oldZoomFactor = zoomFactor(this.scroll);
const newZoomFactor = zoomFactor(this.scroll + event.deltaY);
this.scroll = this.scroll + event.deltaY;
this.offsetX = ???;
this.offsetY = ???;
}
How do I compute the new offsets, based on the previous state, so that the when scrolling the mouse, the point bellow it will appear to be stationary?
Thank you for your answers.
I have finally managed to get it working. Turns out, the answer from the first question I found was correct, but my understanding of SVG viewBox was incorrect and I used bad mouse coordinates.
the offset (min-x and min-y; drawn green) of a viewBox is abbsolute and does not depend on the width and height of the viewBox. The mouse coordinates relative to the SVG element (coordinates drawn in black, SVG element drawn in red) are relative to the size of the viewBox. If I enlarge the viewBox, then the part of the picture I can see inside of it shrinks and 100px line drawn by the mouse will cover more of the image.
If we set the size of the dimensions of the viewBox to be the same as the size of the SVG element (initial state), we have a 1:1 scale between the image and the viewBox (the red rectangle would cover the entire image, bordered black). When we make the viewBox smaller we will not fit the entire image into it and therefore the image will appear to be larger.
If we want to compute the absolute position of our mouse in relation to the entire image we can do it like this (same for Y):
position = offsetX + zoomFactor * mouseX (mouseX relative to the SVG element).
When we zoom, we change the factor, but don't change the position of the mouse. If we want the absolute position under the mouse to remain the same, we have to solve the following set of equations:
oldPosition = oldOffsetX + oldZoomFactor * mouseX
newPosition = newOffsetX + newZoomFactor * mouseX
oldPosition = newPosition
we know the mouse position, both zoom factors and the old offset, therefore we solve for the new offset and get:
newOffsetX = oldOffsetX + mouseX * (oldZoomFactor - newZoomFactor)
which is the final formula and very similar to this answer.
Put together we get the final working solution:
processMouseScroll(event: WheelEvent) {
const oldZoomFactor = zoomFactor(this.scroll);
const newZoomFactor = zoomFactor(this.scroll + event.deltaY);
// mouse position relative to the SVG element
const mouseX = event.pageX - (event.target as SVGElement).getBoundingClientRect().x;
const mouseY = event.pageY - (event.target as SVGElement).getBoundingClientRect().y;
this.scroll = this.scroll + event.deltaY;
this.offsetX = this.offsetX + mouseX * (oldZoomFactor - newZoomFactor);
this.offsetY = this.offsetY + mouseY * (oldZoomFactor - newZoomFactor);
}

Problems Scrolling and Zooming an Image in an WP8.1 App

I'm struggling with the ScrollViewer in my Windows Phone 8.1 (WinRT) app. Basically, what I'm trying to achieve is to retrieve an image using FileOpenPicker, crop the image to a fixed ratio (square) format while letting the user select the part of the image and the zoom level, and then use that image in my app. Perfect would be functionality as in the "People" app where you can add an image to a contact, but I would settle for less if I could somehow get it to work without the ScrollView acting too erratically.
Here is one of the variations I tried:
<ScrollViewer x:Name="SelectedImageScrollViewer"
ZoomMode="Enabled"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Height="300"
Width="300" >
<Image x:Name="SelectedImage"
Source="{Binding SelectedImage}"
MinHeight="300"
MinWidth="300" />
</ScrollViewer>
and in code-behind (in the constructor):
if (SelectedImage.ActualHeight > SelectedImage.ActualWidth) {
SelectedImage.Width = SelectedImageScrollViewer.ViewportWidth;
}
else {
SelectedImage.Height = SelectedImageScrollViewer.ViewportHeight;
}
Like I said, this isn't really working, and there are several problems with it:
ScrollViews have this kind of "rubber band" overscroll functionality built in. While I can agree on platform uniformity, here it isn't helpful, and the mentioned "People" app doesn't have that either.
When the user zooms beyond the MaxZoomLevel, zooming doesn't just stop, but the image drifts away and snaps back after releasing - not a good user experience.
The image can be made smaller than the cropping frame. It should not be possible to reduce the zoom level to the point where the image is not filling the viewport.
The ScrollView does not show the center of the image.
How can I fix those issues, and what would be the best approach for cropping and scaling the image? Would be nice if this was available as part of the SDK as it was in Silverlight (photo chooser).
The following solution provides a reasonably good user experience. Regarding the list of problems:
Apparently cannot be solved using the basic ScrollViewer.
Increasing MaxZoomFactor to something large enough makes it unlikely that the user sees the issue.
After setting the image's smaller dimension to the cropping frame size, a MinZoomFactor of 1 ensures that the image always fills the frame.
The ScrollView's offsets can be set in code behind.
Setting IsScrollInertiaEnabled and IsZoomInertiaEnabled to false removes some of the erratic behavior in scrolling an zooming. Image width and height are set in SelectedImage_SizeChanged because the initial actual dimensions are not available in the constructor (before the page is rendered).
<ScrollViewer Grid.Row="1"
x:Name="SelectedImageScrollViewer"
ZoomMode="Enabled"
IsScrollInertiaEnabled="False"
IsZoomInertiaEnabled="False"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Height="300"
Width="300"
MinZoomFactor="1.0"
MaxZoomFactor="10.0">
<Image x:Name="SelectedImage"
Source="{Binding SelectedImage}"
HorizontalAlignment="Center"
SizeChanged="SelectedImage_SizeChanged" />
</ScrollViewer>
and
private void SelectedImage_SizeChanged(object sender, SizeChangedEventArgs e) {
// Here the proportions of the image are known and the initial size can be set
// to fill the cropping frame depending on the orientation of the image.
if (!_imageProportionsSet) {
if (SelectedImage.ActualWidth != 0) {
double actualHeight = SelectedImage.ActualHeight;
double actualWidth = SelectedImage.ActualWidth;
double viewPortWidth = SelectedImageScrollViewer.ViewportWidth;
double viewPortHeight = SelectedImageScrollViewer.ViewportHeight;
if (actualHeight > actualWidth) {
SelectedImage.Width = viewPortWidth;
double yOffset = (actualHeight - actualWidth) * viewPortWidth / actualHeight;
SelectedImageScrollViewer.ChangeView(0, yOffset, 1);
}
else {
SelectedImage.Height = viewPortHeight;
double xOffset = (actualWidth - actualHeight) * viewPortHeight / actualWidth;
SelectedImageScrollViewer.ChangeView(xOffset, 0, 1);
}
// Do this only once.
_imageProportionsSet = true;
}
}
}
This is workable. If you find any issues with this please don't hesitate to comment or to provide an improved answer.

Scale, Position & Rotate Parent object to make child object take up entire stage

Using the first photo below, let's say:
The red outline is the stage bounds
The gray box is a Sprite on the stage.
The green box is a child of the gray box and has a rotation set.
both display object are anchored at the top-left corner (0,0).
I'd like to rotate, scale, and position the gray box, so the green box fills the stage bounds (the green box and stage have the same aspect ratio).
I can negate the rotation easily enough
parent.rotation = -child.rotation
But the scale and position are proving tricky (because of the rotation). I could use some assistance with the Math involved to calculate the scale and position.
This is what I had tried but didn't produce the results I expected:
gray.scaleX = stage.stageWidth / green.width;
gray.scaleY = gray.scaleX;
gray.x = -green.x;
gray.y = -green.y;
gray.rotation = -green.rotation;
I'm not terribly experienced with Transformation matrices but assume I will need to go that route.
Here is an .fla sample what I'm working with:
SampleFile
You can use this answer: https://stackoverflow.com/a/15789937/1627055 to get some basics. First, you are in need to rotate around the top left corner of the green rectangle, so you use green.x and green.y as center point coordinates. But in between you also need to scale the gray rectangle so that the green rectangle's dimensions get equal to stage. With uniform scaling you don't have to worry about distortion, because if a gray rectangle is scaled uniformly, then a green rectangle will remain a rectangle. If the green rectangle's aspect ratio will be different than what you want it to be, you'd better scale the green rectangle prior to performing this trick. So, you need to first transpose the matrix to offset the center point, then you need to add rotation and scale, then you need to transpose it away. Try this set of code:
var green:Sprite; // your green rect. The code is executed within gray rect
var gr:Number=green.rotation*Math.PI/180; // radians
var gs:Number=stage.stageWidth/green.width; // get scale ratio
var alreadyTurned:Boolean; // if we have already applied the rotation+scale
function turn():void {
if (alreadyTurned) return;
var mat:flash.geom.Matrix=this.transform.matrix;
mat.scale(gs,gs);
mat.translate(-gs*green.x,-gs*green.y);
mat.rotate(-1*gr);
this.transform.matrix=mat;
alreadyTurned=true;
}
Sorry, didn't have time to test, so errors might exist. If yes, try swapping scale, translate and rotate, you pretty much need this set of operations to make it work.
For posterity, here is what I ended up using. I create a sprite/movieClip inside the child (green) box and gave it an instance name of "innerObj" (making it the actually content).
var tmpRectangle:Rectangle = new Rectangle(greenChild.x, greenChild.y, greenChild.innerObj.width * greenChild.scaleX, greenChild.innerObj.height * greenChild.scaleY);
//temporary reset
grayParent.transform.matrix = new Matrix();
var gs:Number=stage.stageHeight/(tmpRectangle.height); // get scale ratio
var mat:Matrix=grayParent.transform.matrix;
mat.scale(gs,gs);
mat.translate(-gs * tmpRectangle.x, -gs * tmpRectangle.y);
mat.rotate( -greenChild.rotation * Math.PI / 180);
grayParent.transform.matrix = mat;
If the registration point of the green box is at one of it's corners (let's say top left), and in order to be displayed this way it has a rotation increased, then the solution is very simple: apply this rotation with negative sign to the parent (if it's 56, add -56 to parent's). This way the child will be with rotation 0 and parent -> -56;
But if there is no rotation applied to the green box, there is almost no solution to your problem, because of wrong registration point. There is no true way to actually determine if the box is somehow rotated or not. And this is why - imagine you have rotated the green box at 90 degrees, but changed it's registration point and thus it has no property for rotation. How could the script understand that this is not it's normal position, but it's flipped? Even if you get the bounds, you will see that it's a regular rectangle, but nobody know which side is it's regular positioned one.
So the short answer is - make the registration point properly, and use rotation in order to display it like in the first image. Then add negative rotation to the parent, and its all good :)
Edit:
I'm uploading an image so I can explain my idea better:
 
As you can see, I've created a green object inside the grey one, and the graphics INSIDE are rotated. The green object itself, has rotation of 0, and origin point - top left.
#Vesper - I don't think that the matrix will fix anything in this situation (remember that the green object has rotation of 0).
Otherwise I agree, that the matrix will do a pretty job, but there are many ways to do it :)

How to continuously update an image based on contentHeight?

I'm trying to draw a solid background color behind an image. The image is a banner that is to take up 33% of the top of my canvas unless that puts it outside of it's aspect ratio. That's where the background color comes in; the background color fills in the right-side of what's remaining after the image fills in it's maximum width without breaking outside of it's 33% height or aspect ratio.
My problem is, with the code below, it only checks the contentHeight once and therefore, if the image height is reduced below what was initially loaded, my background color can be seen below the image (undesired). I can't use my image height since sometimes the height of my image exceeds what is actually shown (because of maintainAspectRatio).
Does anyone know how to obtain a consistent (and bindable) height of the image at all times that is matched to the content and not the container?
I could fully accept that I'm approaching this the wrong way as well so any alternatives to this method that have the same desired result would be much appreciated.
Component:
<mx:Canvas id="mainCanvas" width="100%" height="100%">
<mx:HBox x="0" y="0" backgroundColor="{myBgColor}" width="100%" height="{Math.min(myImg.contentHeight, mainCanvas.height * 0.33)}" />
<mx:Image id="myImg" source="{myImageSrc}" maintainAspectRatio="true" width="100%" height="33%"/>
</mx:Canvas>
My (untested) suggestion is to use the complete handler of Image (spark, not mx):
<fx:Script>
<![CDATA[
function myComplete(event:Event) {
myBox.height = Math.min(myImg.contentHeight, mainCanvas.height * 0.33);
}
]]>
</fx:Script>
<mx:Canvas id="mainCanvas" width="100%" height="100%">
<mx:HBox id="myBox" x="0" y="0" backgroundColor="{myBgColor}" width="100%" />
<s:Image id="myImg" complete="myComplete(event)" source="{myImageSrc}" maintainAspectRatio="true" width="100%" height="33%"/>
</mx:Canvas>
Also I don't understand why you use HBox and not Image's backgroundColor...
Eventually had to use a resize handler on my image since the complete handler never fired for my embedded image source. I was limited to using Flex 3 so no spark components are available.
Solution below:
protected function resizeHandler(event:ResizeEvent):void
{
var newHeight = Math.round(Math.min(myImg.contentHeight, mainCanvas.height * 0.33)) - 1; // Magic -1 because image had a bottom white padding
blueBg.height = newHeight;
}

In AS3/MXML, why does binding a component's height to height / 2 work better than binding it to 50%?

Take the following AS3/MXML code:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark" xmlns="*"
backgroundColor="#000000" showStatusBar="false" width="400" height="400"
minWidth="0" minHeight="0">
<s:Rect width="50%" height="50%">
<s:fill>
<s:SolidColor color="#0000FF"/>
</s:fill>
</s:Rect>
</s:WindowedApplication>
This mostly works. As I increase or decrease the size of the program, the Rect's size will scale to be 50% of the width and height of the WindowedApplication. But as I keep decreasing the window's height, the scaling down stops several pixels short of 0. This is as small as I can get the Rect to be along the y-axis:
After it gets to this point, even if I keep decreasing the size of the WindowedApplication, nothing happens. The Rect stays at exactly the same height until I start increasing the window's size again. What's more is that the Rect is 12 pixels in height, which is a pretty arbitrary number for it to be stopping on.
However if I change:
<s:Rect width="50%" height="50%">
to:
<s:Rect width="{width / 2}" height="{height / 2}">
the problem magically goes away:
The WindowedApplication's height is 5, and the Rect's height is about "two-and-a-half".
Why is there a distinction like this? In the previous example, I did try increasing, then decreasing the size again a few times, even slowly, but it always got stuck in the same spot. Thanks!
There are actually a few separate properties in play here:
height
percentHeight
explicitHeight
height is really just some smoke and mirrors behind percentHeight and explicitHeight.
In the first approach, even though you are specifying the height property as a percent in MXML; the Flex Compiler does some magic behind the scenes to check for the percent character '%' and set the percentHeight property instead of width.
Since no explicit height is given when setting the percent height; the s:Rect's validateSize() method should execute. This method seems like a parallel to the measure() method of the Flex Component lifeCycle.
The validateSize() method does some checking against the explicitMinHeight property
if (!isNaN(explicitMinHeight) && measuredHeight < explicitMinHeight)
measuredHeight = explicitMinHeight;
So, in essence, there is a minimum height; which the component's height will not go lower than.
However, in your second approach, you are not specifying a percentage you are specifying an actual value. As such the explicitHeight value is set. Because an explicit height is set that is given precedence and there is no need to execute the validateSize() method. This line in the validateSize() skips it:
if (!canSkipMeasurement())
measure();
And
protected function canSkipMeasurement():Boolean
{
return !isNaN(explicitWidth) && !isNaN(explicitHeight);
}
So, hopefully that makes sense.
In summary; if you set a percent height; then Flex calculates the height and there is a minimum value. If you set an explicit height; then Flex uses that value and skips its own internal calculations.
[I only did a cursory review of the Flex framework code, so you'll have to review code to dig down deeper]