I'm writing a flutter application and I'm trying to create a tutorial overlay, that will fully show only components that I want the user to be able to interact with.
so I extended CustomPainter class, while getting the Page's context (to get his dimensions) and a list of GlobalKeys (for elements that I want to display and to interact with)
Color colorBlack = Colors.black.withOpacity(0.4);
class CurvePainter extends CustomPainter{
BuildContext context;
List<GlobalKey> globalKeys;
double padding;
#override
void paint(Canvas canvas, Size size) {
final double screenWidth = MediaQuery.of(context).size.width;
final double screenHeight = MediaQuery.of(context).size.height;
Path path = Path()..addRect(Rect.fromLTWH(0, 0, screenWidth, screenHeight));
Set<GlobalKey> keysSet = Set.from(globalKeys);
keysSet.forEach((element){
final List<double> vals = global_key_util.getArea(element);
path = Path.combine(PathOperation.difference,
path,
Path()
..addOval(Rect.fromLTWH(vals[0]-(padding/2),vals[1]-padding/2,vals[2]+padding,vals[3]+padding)));
});
canvas.drawPath(path,
Paint()..color = colorBlack);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return oldDelegate != this;
}
CurvePainter({this.context,this.globalKeys,this.padding=4});
}
so far so good... the button that I want the user to interact with is fully visible, in the initState() of my page I create the overlay and show it.
the problem is that I can't interact with that button!
how can I resolve this ?
thanks
so.. the answer is simply not to use CustomPainter, to use ClipPath with CustomClipper.
in my case I wanted to detect when the user is clicking on the overflow, but to also allow him to interact with the visible widgets behind it.
so I created an InvertedClipper class that extends CustomClipper:
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'HoleArea.dart';
import 'WidgetData.dart';
class InvertedClipper extends CustomClipper<Path> {
final Animation<double> animation;
final List<WidgetData> widgetsData;
double padding;
Function deepEq = const DeepCollectionEquality().equals;
List<HoleArea> areas = [];
InvertedClipper({#required this.padding, this.animation, Listenable reclip,
this.widgetsData}) : super(reclip: reclip) {
if (widgetsData.isNotEmpty) {
widgetsData.forEach((WidgetData widgetData) {
if (widgetData.isEnabled) {
final GlobalKey key = widgetData.key;
if (key == null) {
// throw new Exception("GlobalKey is null!");
} else if (key.currentWidget == null) {
// throw new Exception("GlobalKey is not assigned to a Widget!");
} else {
areas.add(getHoleArea(key: key,shape: widgetData.shape,padding: widgetData.padding));
}
}
});
}
}
#override
Path getClip(Size size) {
Path path = Path();
double animationValue = animation != null ? animation.value : 0;
areas.forEach((HoleArea area) {
switch (area.shape) {
case WidgetShape.Oval: {
path.addOval(Rect.fromLTWH(area.x - (((area.padding + padding) + animationValue*15) / 2), area.y - ((area.padding + padding) + animationValue*15) / 2,
area.width + ((area.padding + padding) + animationValue*15), area.height + ((area.padding + padding) + animationValue*15)));
}
break;
case WidgetShape.Rect: {
path.addRect(Rect.fromLTWH(area.x - (((area.padding + padding) + animationValue*15) / 2), area.y - ((area.padding + padding) + animationValue*15) / 2,
area.width + ((area.padding + padding) + animationValue*15), area.height + ((area.padding + padding) + animationValue*15)));
}
break;
case WidgetShape.RRect: {
path.addRRect(RRect.fromRectAndCorners(Rect.fromLTWH(area.x - (((area.padding + padding) + animationValue*15) / 2), area.y - ((area.padding + padding) + animationValue*15) / 2,
area.width + ((area.padding + padding) + animationValue*15), area.height + ((area.padding + padding) + animationValue*15)),
topLeft: Radius.circular(5.0),
topRight: Radius.circular(5.0),
bottomLeft: Radius.circular(5.0),
bottomRight: Radius.circular(5.0)));
}
break;
}
});
return path
..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height))
..fillType = PathFillType.evenOdd;
}
#override
bool shouldReclip(InvertedClipper oldClipper) {
return !deepEq(oldClipper.areas, areas);
}
}
and I configure a GestureDetector before that to detect when user is clicking on overlay:
return GestureDetector(
onTap: onTap,
child: ClipPath(
clipper: InvertedClipper(
padding: defaultPadding,
animation: animation,
reclip: animationController,
widgetsData: widgetsData),
...
Related
I have created a subclass in Fabric.js 4.3.0 extending fabric.Image, this helps me change the render function so that image will always fit in the bounding box.
I have also created a custom filter for Fabric, using which, by giving 4 corner coordinates, I can distort the image, similar to Photoshop's free transform -> distort tool.
While my code works, the issue is that when I drag the corner controls, the image always resizes from center, moving the other controls points as well.
I am trying to follow the instructions on how to resize objects in fabric using custom control points, the instructions own on polygons, and other shapes, but it does not yield the result required with images.
The result that I want to achieve, is when dragging one of the green control points, the image should distort, but image and the other control points must stay in their own positions without moving, similar to what you see here: https://youtu.be/Pn-9qFNM6Zg?t=274
Here is a JSFIDDLE for the demo: https://jsfiddle.net/human_a/p6d71skm/
fabric.textureSize = 4096;
// Set default filter backend
fabric.filterBackend = new fabric.WebglFilterBackend();
fabric.isWebglSupported(fabric.textureSize);
fabric.Image.filters.Perspective = class extends fabric.Image.filters.BaseFilter {
/**
* Constructor
* #param {Object} [options] Options object
*/
constructor(options) {
super();
if (options) this.setOptions(options);
this.applyPixelRatio();
}
type = 'Perspective';
pixelRatio = fabric.devicePixelRatio;
bounds = {width: 0, height: 0, minX: 0, maxX: 0, minY: 0, maxY: 0};
hasRelativeCoordinates = true;
/**
* Array of attributes to send with buffers. do not modify
* #private
*//** #ts-ignore */
vertexSource = `
precision mediump float;
attribute vec2 aPosition;
attribute vec2 aUvs;
uniform float uStepW;
uniform float uStepH;
varying vec2 vUvs;
vec2 uResolution;
void main() {
vUvs = aUvs;
uResolution = vec2(uStepW, uStepH);
gl_Position = vec4(uResolution * aPosition * 2.0 - 1.0, 0.0, 1.0);
}
`;
fragmentSource = `
precision mediump float;
varying vec2 vUvs;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vUvs);
}
`;
/**
* Return a map of attribute names to WebGLAttributeLocation objects.
*
* #param {WebGLRenderingContext} gl The canvas context used to compile the shader program.
* #param {WebGLShaderProgram} program The shader program from which to take attribute locations.
* #returns {Object} A map of attribute names to attribute locations.
*/
getAttributeLocations(gl, program) {
return {
aPosition: gl.getAttribLocation(program, 'aPosition'),
aUvs: gl.getAttribLocation(program, 'aUvs'),
};
}
/**
* Send attribute data from this filter to its shader program on the GPU.
*
* #param {WebGLRenderingContext} gl The canvas context used to compile the shader program.
* #param {Object} attributeLocations A map of shader attribute names to their locations.
*/
sendAttributeData(gl, attributeLocations, data, type = 'aPosition') {
const attributeLocation = attributeLocations[type];
if (gl[type + 'vertexBuffer'] == null) {
gl[type + 'vertexBuffer'] = gl.createBuffer();
}
gl.bindBuffer(gl.ARRAY_BUFFER, gl[type+'vertexBuffer']);
gl.enableVertexAttribArray(attributeLocation);
gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
}
generateSurface() {
const corners = this.perspectiveCoords;
const surface = verb.geom.NurbsSurface.byCorners(...corners);
const tess = surface.tessellate();
return tess;
}
/**
* Apply the resize filter to the image
* Determines whether to use WebGL or Canvas2D based on the options.webgl flag.
*
* #param {Object} options
* #param {Number} options.passes The number of filters remaining to be executed
* #param {Boolean} options.webgl Whether to use webgl to render the filter.
* #param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered.
* #param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn.
* #param {WebGLRenderingContext} options.context The GL context used for rendering.
* #param {Object} options.programCache A map of compiled shader programs, keyed by filter type.
*/
applyTo(options) {
if (options.webgl) {
const { width, height } = this.getPerspectiveBounds();
options.context.canvas.width = width;
options.context.canvas.height = height;
options.destinationWidth = width;
options.destinationHeight = height;
this.hasRelativeCoordinates && this.calculateCoordsByCorners();
this._setupFrameBuffer(options);
this.applyToWebGL(options);
this._swapTextures(options);
}
}
applyPixelRatio(coords = this.perspectiveCoords) {
for(let i = 0; i < coords.length; i++) {
coords[i][0] *= this.pixelRatio;
coords[i][1] *= this.pixelRatio;
}
return coords;
}
getPerspectiveBounds(coords = this.perspectiveCoords) {
coords = this.perspectiveCoords.slice().map(c => (
{
x: c[0],
y: c[1],
}
));
this.bounds.minX = fabric.util.array.min(coords, 'x') || 0;
this.bounds.minY = fabric.util.array.min(coords, 'y') || 0;
this.bounds.maxX = fabric.util.array.max(coords, 'x') || 0;
this.bounds.maxY = fabric.util.array.max(coords, 'y') || 0;
this.bounds.width = Math.abs(this.bounds.maxX - this.bounds.minX);
this.bounds.height = Math.abs(this.bounds.maxY - this.bounds.minY);
return {
width: this.bounds.width,
height: this.bounds.height,
minX: this.bounds.minX,
maxX: this.bounds.maxX,
minY: this.bounds.minY,
maxY: this.bounds.maxY,
};
}
/**
* #description coordinates are coming in relative to mockup item sections
* the following function normalizes the coords based on canvas corners
*
* #param {number[]} coords
*/
calculateCoordsByCorners(coords = this.perspectiveCoords) {
for(let i = 0; i < coords.length; i++) {
coords[i][0] -= this.bounds.minX;
coords[i][1] -= this.bounds.minY;
}
}
/**
* Apply this filter using webgl.
*
* #param {Object} options
* #param {Number} options.passes The number of filters remaining to be executed
* #param {Boolean} options.webgl Whether to use webgl to render the filter.
* #param {WebGLTexture} options.originalTexture The texture of the original input image.
* #param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered.
* #param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn.
* #param {WebGLRenderingContext} options.context The GL context used for rendering.
* #param {Object} options.programCache A map of compiled shader programs, keyed by filter type.
*/
applyToWebGL(options) {
const gl = options.context;
const shader = this.retrieveShader(options);
const tess = this.generateSurface(options.sourceWidth, options.sourceHeight);
const indices = new Uint16Array(_.flatten(tess.faces));
// Clear the canvas first
this.clear(gl); // !important
// bind texture buffer
this.bindTexture(gl, options);
gl.useProgram(shader.program);
// create the buffer
this.indexBuffer(gl, indices);
this.sendAttributeData(gl, shader.attributeLocations, new Float32Array(_.flatten(tess.points)), 'aPosition');
this.sendAttributeData(gl, shader.attributeLocations, new Float32Array(_.flatten(tess.uvs)), 'aUvs');
gl.uniform1f(shader.uniformLocations.uStepW, 1 / gl.canvas.width);
gl.uniform1f(shader.uniformLocations.uStepH, 1 / gl.canvas.height);
this.sendUniformData(gl, shader.uniformLocations);
gl.viewport(0, 0, options.destinationWidth, options.destinationHeight);
// enable indices up to 4294967296 for webGL 1.0
gl.getExtension('OES_element_index_uint');
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
clear(gl) {
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
bindTexture(gl, options) {
if (options.pass === 0 && options.originalTexture) {
gl.bindTexture(gl.TEXTURE_2D, options.originalTexture);
} else {
gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture);
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
indexBuffer(gl, data) {
const indexBuffer = gl.createBuffer();
// make this buffer the current 'ELEMENT_ARRAY_BUFFER'
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// Fill the current element array buffer with data
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
}
};
/**
* Returns filter instance from an object representation
* #static
* #param {Object} object Object to create an instance from
* #param {function} [callback] to be invoked after filter creation
* #return {fabric.Image.filters.Perspective} Instance of fabric.Image.filters.Perspective
*/
fabric.Image.filters.Perspective.fromObject = fabric.Image.filters.BaseFilter.fromObject;
/**
* Photo subclass
* #class fabric.Photo
* #extends fabric.Photo
* #return {fabric.Photo} thisArg
*
*/
fabric.Photo = class extends fabric.Image {
type = 'photo';
repeat = 'no-repeat';
fill = 'transparent';
initPerspective = true;
cacheProperties = fabric.Image.prototype.cacheProperties.concat('perspectiveCoords');
constructor(src, options) {
super(options);
if (options) this.setOptions(options);
this.on('added', () => {
const image = new Image();
image.setAttribute('crossorigin', 'anonymous');
image.onload = () => {
this._initElement(image, options);
this.width = image.width / 2;
this.height = image.height / 2;
this.loaded = true;
this.setCoords();
this.fire('image:loaded');
};
image.src = src;
this.on('image:loaded', () => {
!this.perspectiveCoords && this.getInitialPerspective();
this.togglePerspective();
this.canvas.requestRenderAll();
});
});
}
cacheProperties = fabric.Image.prototype.cacheProperties.concat('perspectiveCoords');
/**
* #private
* #param {CanvasRenderingContext2D} ctx Context to render on
*//** #ts-ignore */
_render(ctx) {
fabric.util.setImageSmoothing(ctx, this.imageSmoothing);
if (this.isMoving !== true && this.resizeFilter && this._needsResize()) {
this.applyResizeFilters();
}
this._stroke(ctx);
this._renderPaintInOrder(ctx);
}
/**
* #private
* #param {CanvasRenderingContext2D} ctx Context to render on
*//** #ts-ignore */
_renderFill(ctx) {
var elementToDraw = this._element;
if (!elementToDraw) return;
ctx.save();
const elWidth = elementToDraw.naturalWidth || elementToDraw.width;
const elHeight = elementToDraw.naturalHeight || elementToDraw.height;
const width = this.width;
const height = this.height;
ctx.translate(-width / 2, -height / 2);
// get the scale
const scale = Math.min(width / elWidth, height / elHeight);
// get the top left position of the image
const x = (width / 2) - (elWidth / 2) * scale;
const y = (height / 2) - (elHeight / 2) * scale;
ctx.drawImage(elementToDraw, x, y, elWidth * scale, elHeight * scale);
ctx.restore();
}
togglePerspective(mode = true) {
this.set('perspectiveMode', mode);
// this.set('hasBorders', !mode);
if (mode === true) {
this.set('layout', 'fit');
var lastControl = this.perspectiveCoords.length - 1;
this.controls = this.perspectiveCoords.reduce((acc, coord, index) => {
const anchorIndex = index > 0 ? index - 1 : lastControl;
let name = `prs${index + 1}`;
acc[name] = new fabric.Control({
name,
x: -0.5,
y: -0.5,
actionHandler: this._actionWrapper(anchorIndex, (_, transform, x, y) => {
const target = transform.target;
const localPoint = target.toLocalPoint(new fabric.Point(x, y), 'left', 'top');
coord[0] = localPoint.x / target.scaleX * fabric.devicePixelRatio;
coord[1] = localPoint.y / target.scaleY * fabric.devicePixelRatio;
target.setCoords();
target.applyFilters();
return true;
}),
positionHandler: function (dim, finalMatrix, fabricObject) {
const zoom = fabricObject.canvas.getZoom();
const scalarX = fabricObject.scaleX * zoom / fabric.devicePixelRatio;
const scalarY = fabricObject.scaleY * zoom / fabric.devicePixelRatio;
var point = fabric.util.transformPoint({
x: this.x * dim.x + this.offsetX + coord[0] * scalarX,
y: this.y * dim.y + this.offsetY + coord[1] * scalarY,
}, finalMatrix
);
return point;
},
cursorStyleHandler: () => 'cell',
render: function(ctx, left, top, _, fabricObject) {
const zoom = fabricObject.canvas.getZoom();
const scalarX = fabricObject.scaleX * zoom / fabric.devicePixelRatio;
const scalarY = fabricObject.scaleY * zoom / fabric.devicePixelRatio;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.strokeStyle = 'green';
if (fabricObject.perspectiveCoords[index + 1]) {
ctx.strokeStyle = 'green';
ctx.lineTo(
(fabricObject.perspectiveCoords[index + 1][0] - coord[0]) * scalarX,
(fabricObject.perspectiveCoords[index + 1][1] - coord[1]) * scalarY,
);
} else {
ctx.lineTo(
(fabricObject.perspectiveCoords[0][0] - coord[0]) * scalarX,
(fabricObject.perspectiveCoords[0][1] - coord[1]) * scalarY,
);
}
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = 'green';
ctx.fill();
ctx.stroke();
ctx.restore();
},
offsetX: 0,
offsetY: 0,
actionName: 'perspective-coords',
});
return acc;
}, {});
} else {
this.controls = fabric.Photo.prototype.controls;
}
this.canvas.requestRenderAll();
}
_actionWrapper(anchorIndex, fn) {
return function(eventData, transform, x, y) {
if (!transform || !eventData) return;
const { target } = transform;
target._resetSizeAndPosition(anchorIndex);
const actionPerformed = fn(eventData, transform, x, y);
return actionPerformed;
};
}
/**
* #description manually reset the bounding box after points update
*
* #see http://fabricjs.com/custom-controls-polygon
* #param {number} index
*/
_resetSizeAndPosition = (index, apply = true) => {
const absolutePoint = fabric.util.transformPoint({
x: this.perspectiveCoords[index][0],
y: this.perspectiveCoords[index][1],
}, this.calcTransformMatrix());
this._setPositionDimensions({});
const penBaseSize = this._getNonTransformedDimensions();
const newX = (this.perspectiveCoords[index][0]) / penBaseSize.x;
const newY = (this.perspectiveCoords[index][1]) / penBaseSize.y;
this.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
apply && this._applyPointsOffset();
}
/**
* This is modified version of the internal fabric function
* this helps determine the size and the location of the path
*
* #param {object} options
*/
_setPositionDimensions(options) {
const { left, top, width, height } = this._calcDimensions(options);
this.width = width;
this.height = height;
var correctLeftTop = this.translateToGivenOrigin(
{
x: left,
y: top,
},
'left',
'top',
this.originX,
this.originY
);
if (typeof options.left === 'undefined') {
this.left = correctLeftTop.x;
}
if (typeof options.top === 'undefined') {
this.top = correctLeftTop.y;
}
this.pathOffset = {
x: left,
y: top,
};
return { left, top, width, height };
}
/**
* #description this is based on fabric.Path._calcDimensions
*
* #private
*/
_calcDimensions() {
const coords = this.perspectiveCoords.slice().map(c => (
{
x: c[0] / fabric.devicePixelRatio,
y: c[1] / fabric.devicePixelRatio,
}
));
const minX = fabric.util.array.min(coords, 'x') || 0;
const minY = fabric.util.array.min(coords, 'y') || 0;
const maxX = fabric.util.array.max(coords, 'x') || 0;
const maxY = fabric.util.array.max(coords, 'y') || 0;
const width = Math.abs(maxX - minX);
const height = Math.abs(maxY - minY);
return {
left: minX,
top: minY,
width: width,
height: height,
};
}
/**
* #description This is modified version of the internal fabric function
* this subtracts the path offset from each path points
*/
_applyPointsOffset() {
for (let i = 0; i < this.perspectiveCoords.length; i++) {
const coord = this.perspectiveCoords[i];
coord[0] -= this.pathOffset.x;
coord[1] -= this.pathOffset.y;
}
}
/**
* #description generate the initial coordinates for warping, based on image dimensions
*
*/
getInitialPerspective() {
let w = this.getScaledWidth();
let h = this.getScaledHeight();
const perspectiveCoords = [
[0, 0], // top left
[w, 0], // top right
[w, h], // bottom right
[0, h], // bottom left
];
this.perspectiveCoords = perspectiveCoords;
const perspectiveFilter = new fabric.Image.filters.Perspective({
hasRelativeCoordinates: false,
pixelRatio: fabric.devicePixelRatio, // the Photo is already retina ready
perspectiveCoords
});
this.filters.push(perspectiveFilter);
this.applyFilters();
return perspectiveCoords;
}
};
/**
* Creates an instance of fabric.Photo from its object representation
* #static
* #param {Object} object Object to create an instance from
* #param {Function} callback Callback to invoke when an image instance is created
*/
fabric.Photo.fromObject = function(_object, callback) {
const object = fabric.util.object.clone(_object);
object.layout = _object.layout;
fabric.util.loadImage(object.src, function(img, isError) {
if (isError) {
callback && callback(null, true);
return;
}
fabric.Photo.prototype._initFilters.call(object, object.filters, function(filters) {
object.filters = filters || [];
fabric.Photo.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) {
object.resizeFilter = resizeFilters[0];
fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
object.clipPath = enlivedProps[0];
var image = new fabric.Photo(img, object);
callback(image, false);
});
});
});
}, null, object.crossOrigin || 'anonymous');
};
const canvas = new fabric.Canvas(document.getElementById('canvas'), {
backgroundColor: 'white',
enableRetinaScaling: true,
});
function resizeCanvas() {
canvas.setWidth(window.innerWidth);
canvas.setHeight(window.innerHeight);
}
resizeCanvas();
window.addEventListener('resize', () => resizeCanvas(), false);
const photo = new fabric.Photo('https://cdn.artboard.studio/private/5cb9c751-5f17-4062-adb7-6ec2c137a65d/user_uploads/5bafe170-1580-4d6b-a3be-f5cdce22d17d-asdasdasd.jpg', {
left: canvas.getWidth() / 2,
top: canvas.getHeight() / 2,
originX: 'center',
originY: 'center',
});
canvas.add(photo);
canvas.setActiveObject(photo);
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/verb-nurbs-web#2.1.3/build/js/verb.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fabric#4.3.0/dist/fabric.min.js"></script>
<canvas id="canvas"></canvas>
I suspect that the reference to absolutePoint in _resetSizeAndPosition needs to take into account the origin for the image and that there is a simple fix to this issue. However, I didn't find a good way to do this and resorted to manually "correcting" this issue in _resetSizeAndPosition.
The modified version of _resetSizeAndPosition looks like so:
_resetSizeAndPosition = (index, apply = true) => {
const absolutePoint = fabric.util.transformPoint({
x: this.perspectiveCoords[index][0],
y: this.perspectiveCoords[index][1],
}, this.calcTransformMatrix());
let { height, width, left, top } = this._calcDimensions({});
const widthDiff = (width - this.width) / 2;
if ((left < 0 && widthDiff > 0) || (left > 0 && widthDiff < 0)) {
absolutePoint.x -= widthDiff;
} else {
absolutePoint.x += widthDiff;
}
const heightDiff = (height - this.height) / 2;
if ((top < 0 && heightDiff > 0) || (top > 0 && heightDiff < 0)) {
absolutePoint.y -= heightDiff;
} else {
absolutePoint.y += heightDiff;
}
this._setPositionDimensions({});
const penBaseSize = this._getNonTransformedDimensions();
const newX = (this.perspectiveCoords[index][0]) / penBaseSize.x;
const newY = (this.perspectiveCoords[index][1]) / penBaseSize.y;
this.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
apply && this._applyPointsOffset();
}
The basic principle for this approach is that the left and top properties of the object are never being updated. This can be seen in your example through the console by modifying the image and checking the properties on the image. Therefore, we need to apply a correction to the position properties based on the changing width and height. This ensures that other points stay fixed in place, since we compensate for the changing height and width of the image in its position.
By comparing the values of width and this.width it's possible to determine if the image is increasing or decreasing in size. The value of left indicates whether the stretch is occurring to the left or right side of the image. If the user is stretching the image to the left or shrinking it from the right then we need. By combining the conditions for these, we can tell how we need to modify the position of the image to compensate. The same approach used for the horizontal values is also applied to the vertical values.
JSFiddle: https://jsfiddle.net/0x8caow6/
I have a CustomPainter that looks like this:
class MyPainter extends CustomPainter {
Offset left, top, right, bottom;
MyPainter({this.left, this.top, this.right, this.bottom});
#override
void paint(Canvas canvas, Size size) {
Paint pp = Paint()
..color = Colors.blue
..strokeCap = StrokeCap.round
..strokeWidth = 10;
Paint p = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 2;
Path ph = Path();
ph.moveTo(left.dx, left.dy);
ph.quadraticBezierTo(top.dx, top.dy, right.dx, right.dy);
canvas.drawPoints(PointMode.points, [left, top, right, bottom], pp);
canvas.drawPath(ph, p);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
And this is the result: https://imgur.com/a/m4i6WEA
However I want the curve line, pass the control point. Something like this: https://imgur.com/a/cumbAVz
How can I do that?!
You need to calculate internal control point P1 coordinates based on needed point top
At first, roughly evaluate parameter t for that point. Let it is 1/2 (if top lies near the middle of curve). Using quadratic Bezier formula:
P(t) = P0 * (1-t)^2 + 2 * P1 * t * (1-t) + P2 * t^2
top {for t=1/2} = P0 * 1/4 + P1 * 1/2 + P2 * 1/4
P1 = 2 * top - (P0 + P2) / 2
in components:
P1.dx = 2 * top.dx - (left.dx + right.dx) / 2
P1.dy = 2 * top.dy - (left.dy + right.dy) / 2
and finally
ph.quadraticBezierTo(P1.dx, P1.dy, right.dx, right.dy);
Since swipe() is deprecated, I am unable to swipe the screen from Left to Right. My App has 4 banners in it and I want to swipe to view all the banners.
This applies in all directions:
enum:
public enum DIRECTION {
DOWN, UP, LEFT, RIGHT;
}
actual code:
public static void swipe(MobileDriver driver, DIRECTION direction, long duration) {
Dimension size = driver.manage().window().getSize();
int startX = 0;
int endX = 0;
int startY = 0;
int endY = 0;
switch (direction) {
case RIGHT:
startY = (int) (size.height / 2);
startX = (int) (size.width * 0.90);
endX = (int) (size.width * 0.05);
new TouchAction(driver)
.press(startX, startY)
.waitAction(Duration.ofMillis(duration))
.moveTo(endX, startY)
.release()
.perform();
break;
case LEFT:
startY = (int) (size.height / 2);
startX = (int) (size.width * 0.05);
endX = (int) (size.width * 0.90);
new TouchAction(driver)
.press(startX, startY)
.waitAction(Duration.ofMillis(duration))
.moveTo(endX, startY)
.release()
.perform();
break;
case UP:
endY = (int) (size.height * 0.70);
startY = (int) (size.height * 0.30);
startX = (size.width / 2);
new TouchAction(driver)
.press(startX, startY)
.waitAction(Duration.ofMillis(duration))
.moveTo(startX, endY)
.release()
.perform();
break;
case DOWN:
startY = (int) (size.height * 0.70);
endY = (int) (size.height * 0.30);
startX = (size.width / 2);
new TouchAction(driver)
.press(startX, startY)
.waitAction(Duration.ofMillis(duration))
.moveTo(startX, endY)
.release()
.perform();
break;
}
}
usage:
swipe(driver,DIRECTION.RIGHT);
Hope this helps,
try below method. It works with Appium 1.16.0 version.
I created this method to swipe left or right based on a particular element location on the screen. It takes 3 parameters
Element X: It is the X coordinate of the element on which swipe touch action needs to be performed.
Element Y: It is the Y coordinate of the element.
Direction: Left/Right
//method to left and right swipe on the screen based on coordinates
public void swipeAction(int Xcoordinate, int Ycoordinate, String direction) {
//get device width and height
Dimension dimension = driver.manage().window().getSize();
int deviceHeight = dimension.getHeight();
int deviceWidth = dimension.getWidth();
System.out.println("Height x Width of device is: " + deviceHeight + " x " + deviceWidth);
switch (direction) {
case "Left":
System.out.println("Swipe Right to Left");
//define starting and ending X and Y coordinates
int startX=deviceWidth - Xcoordinate;
int startY=Ycoordinate; // (int) (height * 0.2);
int endX=Xcoordinate;
int endY=Ycoordinate;
//perform swipe from right to left
new TouchAction((AppiumDriver) driver).longPress(PointOption.point(startX, startY)).moveTo(PointOption.point(endX, endY)).release().perform();
break;
case "Right":
System.out.println("Swipe Left to Right");
//define starting X and Y coordinates
startX=Xcoordinate;
startY=Ycoordinate;
endX=deviceWidth - Xcoordinate;
endY=Ycoordinate;
//perform swipe from left to right
new TouchAction((AppiumDriver) driver).longPress(PointOption.point(startX, startY)).moveTo(PointOption.point(endX, endY)).release().perform();
break;
}
}
To fetch the element X,Y coordinates. try below methods
int elementX= driver.findElement(elementLocator).getLocation().getX();
int elementY= driver.findElement(elementLocator).getLocation().getY();
Assuming you created driver instance of AndroidDriver you can swipe left:
// Get location of element you want to swipe
WebElement banner = driver.findElement(<your_locator>);
Point bannerPoint = banner.getLocation();
// Get size of device screen
Dimension screenSize = driver.manage().window().getSize();
// Get start and end coordinates for horizontal swipe
int startX = Math.toIntExact(Math.round(screenSize.getWidth() * 0.8));
int endX = 0;
TouchAction action = new TouchAction(driver);
action
.press(PointOption.point(startX, bannerPoint.getY()))
.waitAction(WaitOptions.waitOptions(Duration.ofMillis(500)))
.moveTo(PointOption.point(endX, bannerPoint.getY()))
.release();
driver.performTouchAction(action);
Use latest appium-java-client 6.1.0 and Appium 1.8.x server
This should work,
Dimension size = driver.manage().window().getSize();
System.out.println(size.height+"height");
System.out.println(size.width+"width");
System.out.println(size);
int startPoint = (int) (size.width * 0.99);
int endPoint = (int) (size.width * 0.15);
int ScreenPlace =(int) (size.height*0.40);
int y=(int)size.height*20;
TouchAction ts = new TouchAction(driver);
//for(int i=0;i<=3;i++) {
ts.press(PointOption.point(startPoint,ScreenPlace ))
.waitAction(WaitOptions.waitOptions(Duration.ofMillis(1000)))
.moveTo(PointOption.point(endPoint,ScreenPlace )).release().perform();
This is for iOS Mobile:
//Here i am trying to swipe list of images from right to left
//First i am getting parent element (table/cell) id
//Then using predicatestring am searching for the element present or not then trying to click
List<MobileElement> ele = getMobileElement(listBtnQuickLink).findElements(By.xpath(".//XCUIElementTypeButton"));
for(int i=1 ;i<=20;i++) {
MobileElement ele1 = ele.get(i);
String parentID = getMobileElement(listBtnQuickLink).getId();
HashMap<String, String> scrollObject = new HashMap<String, String>();
scrollObject.put("element", parentID); //This is parent element id (not same element)
scrollObject.put("predicateString", "label == '"+ele1.getText()+"'");
scrollObject.put("direction", "left");
driver.executeScript("mobile:swipe", scrollObject); // scroll to the target element
System.out.println("Element is visible : "+ele1.isDisplayed());
}
Unfortunately i was noted that TouchAction doesn't work on Android 11 with Selenium 4. So if you use Selenide and Appium you can try this:
public class SwipeToLeft implements Command<SelenideElement> {
#Nullable
#Override
public SelenideElement execute(SelenideElement proxy, WebElementSource locator, #Nullable Object[] args) throws IOException {
Selenide.sleep(2000);
var driver = WebDriverRunner.getWebDriver();
var element = proxy.getWrappedElement();
((JavascriptExecutor) driver).executeScript("mobile: swipeGesture", ImmutableMap.of(
"elementId", ((RemoteWebElement) element).getId(),
"direction", "left",
"percent", 0.75
));
return proxy;
}
}
And then you can use:
$('your seleniumLocator').shouldBe(visible).execute(new SwipeToLeft());
I have a Paint object and I'm trying to use it to paint an Arc Gradient using canvas.drawArc, but the only way to do this (at least according to my research) is to use a Shader, but to get a Shader from a Gradient object, you have to use Gradient.createShader(Rect rect), which takes a rectangle. My question is, is there any way to create a shader for an Arc and not a Rectangle? Here's what I have so far for reference:
Paint paint = new Paint()
..color = bgColor
..strokeCap = StrokeCap.round
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..shader = new Gradient.radial(size.width / 2.0, size.height / 2.0, size.height / 3.0, Colors.transparent, timerColor, TileMode.mirror).createShader(/* I don't have a rect object */);
canvas.drawArc(..., paint);
The Rectangle that you need is actually a square into which the circle that you are drawing would fit. The arc is just a slice of pie from that circle swept through so many radians. Create this square using Rect.fromCircle, using the centre and radius. You then use this square when creating the gradient and drawing the arc.
Here's an example
import 'dart:math';
import 'package:flutter/material.dart';
class X1Painter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// create a bounding square, based on the centre and radius of the arc
Rect rect = new Rect.fromCircle(
center: new Offset(165.0, 55.0),
radius: 180.0,
);
// a fancy rainbow gradient
final Gradient gradient = new RadialGradient(
colors: <Color>[
Colors.green.withOpacity(1.0),
Colors.green.withOpacity(0.3),
Colors.yellow.withOpacity(0.2),
Colors.red.withOpacity(0.1),
Colors.red.withOpacity(0.0),
],
stops: [
0.0,
0.5,
0.7,
0.9,
1.0,
],
);
// create the Shader from the gradient and the bounding square
final Paint paint = new Paint()..shader = gradient.createShader(rect);
// and draw an arc
canvas.drawArc(rect, pi / 4, pi * 3 / 4, true, paint);
}
#override
bool shouldRepaint(X1Painter oldDelegate) {
return true;
}
}
class X1Demo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: const Text('Arcs etc')),
body: new CustomPaint(
painter: new X1Painter(),
),
);
}
}
void main() {
runApp(
new MaterialApp(
theme: new ThemeData.dark(),
home: new X1Demo(),
),
);
}
My SweepGradient version.
Complete example:
import 'dart:math' as math;
import 'package:flutter/material.dart';
class GradientArcPainterDemo extends StatefulWidget {
const GradientArcPainterDemo({
Key key,
}) : super(key: key);
#override
GradientArcPainterDemoState createState() => GradientArcPainterDemoState();
}
class GradientArcPainterDemoState extends State<GradientArcPainterDemo> {
double _progress = 0.9;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(title: const Text('GradientArcPainter Demo')),
body: GestureDetector(
onTap: () {
setState(() {
_progress += 0.1;
});
},
child: Center(
child: SizedBox(
width: 200.0,
height: 200.0,
child: CustomPaint(
painter: GradientArcPainter(
progress: _progress,
startColor: Colors.blue,
endColor: Colors.red,
width: 8.0,
),
child: Center(child: Text('$_progress')),
),
),
),
),
);
}
}
class GradientArcPainter extends CustomPainter {
const GradientArcPainter({
#required this.progress,
#required this.startColor,
#required this.endColor,
#required this.width,
}) : assert(progress != null),
assert(startColor != null),
assert(endColor != null),
assert(width != null),
super();
final double progress;
final Color startColor;
final Color endColor;
final double width;
#override
void paint(Canvas canvas, Size size) {
final rect = new Rect.fromLTWH(0.0, 0.0, size.width, size.height);
final gradient = new SweepGradient(
startAngle: 3 * math.pi / 2,
endAngle: 7 * math.pi / 2,
tileMode: TileMode.repeated,
colors: [startColor, endColor],
);
final paint = new Paint()
..shader = gradient.createShader(rect)
..strokeCap = StrokeCap.butt // StrokeCap.round is not recommended.
..style = PaintingStyle.stroke
..strokeWidth = width;
final center = new Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width / 2, size.height / 2) - (width / 2);
final startAngle = -math.pi / 2;
final sweepAngle = 2 * math.pi * progress;
canvas.drawArc(new Rect.fromCircle(center: center, radius: radius),
startAngle, sweepAngle, false, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Result:
I have a implemented a ListField on BlackBerry. How do I add 3 labels to the list?
Follow this tutorial:
http://berrytutorials.blogspot.com/2009/11/create-custom-listfield-change.html
After completed, modify the extended ListField class by adding some extra components to your list (graphics.drawText(CALLBACK OBJECT, X, Y)). Change the String callback to an object of your type(or just an Array) with the availability for more elements.
EXAMPLE OF THE PAINT METHOD INSIDE THE EXTENDED LISTFIELD CLASS:
public void paint(Graphics graphics) {
int width = (int) (300 * resizeWidthFactor);
// Get the current clipping region
XYRect redrawRect = graphics.getClippingRect();
// Side lines
// graphics.setColor(Color.GRAY);
// graphics.drawLine(0, 0, 0, redrawRect.height);
// graphics.setColor(Color.GRAY);
// graphics.drawLine(redrawRect.width-1, 0, redrawRect.width-1,
// redrawRect.height);
if (redrawRect.y < 0) {
throw new IllegalStateException("Error with clipping rect.");
}
// Determine the start location of the clipping region and end.
int rowHeight = getRowHeight();
int curSelected;
// If the ListeField has focus determine the selected row.
if (hasFocus) {
curSelected = getSelectedIndex();
} else {
curSelected = -1;
}
int startLine = redrawRect.y / rowHeight;
int endLine = (redrawRect.y + redrawRect.height - 1) / rowHeight;
endLine = Math.min(endLine, getSize() - 1);
int y = (startLine * rowHeight) + heightMargin;
// Setup the data used for drawing.
int[] yInds = new int[] { y, y, y + rowHeight, y + rowHeight };
int[] xInds = new int[] { 0, width, width, 0 };
// Set the callback - assuming String values.
ListFieldCallback callBack = this.getCallback();
// Draw each row
for (; startLine <= endLine; ++startLine) {
// If the line we're drawing is the currentlySelected line then draw the
// fill path in LIGHTYELLOW and the
// font text in Black.
//OBJECT OF OWN TYPE FOR MULTIPLE PARAMETERS
ProductDetails data = (ProductDetails) callBack.get(this, startLine);
String productDescription = "";
String errorDescription = "";
if (data.isError()) {
errorDescription = TextLineSplitter.wrapString1Line(data.getErrorMessage(), (int) ((300 - (2 * widthMargin)) * resizeWidthFactor), getFont());
} else {
productDescription = TextLineSplitter.wrapString1Line(data.getProductDesc(), (int) ((300 - (2 * widthMargin)) * resizeWidthFactor), getFont());
}
// Set differences by row (selected or not)
if (startLine == curSelected) {
graphics.setColor(Color.WHITE);
} else {
// Draw the odd or selected rows.
graphics.setColor(Color.BLACK);
}
// Set text values
if (!data.isError()) {
// If no error found
//FIRST LABEL
graphics.setFont(getFont().derive(Font.BOLD));
graphics.drawText("Result search " + Integer.toString(data.getSearchId()) + ":", widthMargin, yInds[0]);
graphics.drawText(data.getManufacturerItemIdentifier(), widthMargin + (int) (140 * resizeWidthFactor), yInds[0]);
//SECOND LABEL
graphics.setFont(getFont().derive(Font.PLAIN));
graphics.drawText(productDescription, widthMargin, yInds[0] + (int) (20 * resizeHeightFactor));
} else {
// Error found
graphics.setColor(Color.GRAY);
graphics.setFont(getFont().derive(Font.BOLD));
graphics.drawText("Result search " + Integer.toString(data.getSearchId()) + ":", widthMargin, yInds[0]);
graphics.setFont(getFont().derive(Font.PLAIN));
graphics.drawText(errorDescription, widthMargin, yInds[0] + (int) (20 * resizeHeightFactor));
}
// Bottom line
if (startLine == endLine) {
graphics.setColor(Color.GRAY);
graphics.drawLine(0, yInds[2] - (heightMargin + 1), (int) (300 * resizeWidthFactor), yInds[2] - (heightMargin + 1));
}
// Horizontal lines
graphics.setColor(Color.GRAY);
graphics.drawLine(0, yInds[0] - heightMargin, (int) (300 * resizeWidthFactor), yInds[0] - heightMargin);
// Assign new values to the y axis moving one row down.
y += rowHeight;
yInds[0] = y;
yInds[1] = yInds[0];
yInds[2] = y + rowHeight;
yInds[3] = yInds[2];
}
// super.paint(graphics);
}